OCI Language Como Suporte ao Profissional Autista

OCI Language Como Suporte ao Profissional Autista


Não é fácil entender a percepção de um cliente sobre um trabalho, especialmente cara a cara. Nesse ponto, um feedback escrito é uma solução mais confortável para ambos os lados. Mas e quando não estamos falando de só um cliente? E quando temos dezenas? Centenas? Milhares?

Já é possível pedir para uma inteligência artificial analisar o sentimento geral baseado num feedback escrito. Essa análise de sentimento simples devolve uma nota geral: positivo, negativo, neutro ou misto. Mas nem sempre isso é suficiente para uma avaliação correta.

E se não estivermos tratando de clientes, mas de um professor e seus alunos?

O pipeline que vou montar aqui usa quatro serviços Oracle: imagens de cada um dos feedbacks ficarão no Object Storage, passarão por OCR com OCI Vision, e o sentimento de cada um deles será analisado com OCI Language e o gráfico volta para o bucket. Tudo isso sem que o professor precise instalar nada.

O problema

Um professor de história está no espectro autista. Estar no espectro não afeta sua capacidade de ensinar ou o domínio do conteúdo, mas muitas vezes ele sente que sua leitura de sinais sociais em sala de aula pode não ser realista: a postura corporal de uma turma está dizendo que estão tensos ou concentrados, o silêncio indica desmotivação ou foco, a energia do grupo é sinal de motivação ou descontrole?

Para alguém neurotípico, muitas vezes essas leituras não demandam esforço. Para alguém no espectro o custo cognitivo pode ser muito maior, o que pode acabar numa situação não esperada.

Muitas vezes, o resultado disso pode ser apenas incerteza. O professor termina o ano letivo sem saber como cada turma viveu aquele período com ele.

A abordagem

Para entender melhor o andamento de suas turmas, na última prova do ano, junto com as questões, cada aluno recebeu um envelope sem identificação. Dentro haviam três perguntas:

  1. O que você achou do nosso ano letivo em História?
  2. Você teve algum problema com o professor?
  3. Como você avalia o seu aprendizado?

Os alunos são orientados a depositar os envelopes numa caixa após o fim da prova, sem identificação nenhuma. Essa assimetria muda o que as pessoas estão dispostas a dizer.

O problema começa depois. Esse teste é feito em três turmas, trinta envelopes por turma, três respostas cada: são noventa textos livres. Extrair padrões e chegar a uma conclusão do sentimento de cada turma não é simples, ainda mais quando interpretar sinais sociais implícitos já exige mais esforço.

O pipeline

Envelope físico (foto ou scan)
         ↓
OCI Object Storage  ←  professor faz upload pelo console
         ↓
OCI Vision OCR  →  texto extraído
         ↓
OCI Language  →  sentimento por aspecto
         ↓
Gráfico gerado via Python no Cloud Shell
         ↓
OCI Object Storage  ←  resultado salvo no bucket

Quatro serviços Oracle, todos dentro da infraestrutura OCI.

Uma detalhe sobre OCR: a precisão depende da legibilidade do que os alunos escreveram. Como são todos manuscritos, num caso como esse, seria interessante orientar os alunos a usar letra de forma ou caprichar na letra.

Os dados de teste

Para testar e demonstrar os resultados, gerei 90 feedbacks diferentes por AI e separei em três turmas fictícias de trinta alunos, com respostas em português brasileiro simulando perfis distintos.

"1","Gostei. História Antiga foi o meu ponto favorito do ano.","Não tive problema. Ele respeita quando preciso de tempo para responder.","Foi muito positivo. O professor ajudou a estruturar o raciocínio histórico."

Na prática, nesse pipeline, as respostas viriam das respostas reais processadas pelo OCR. Cada CSV tem quatro colunas: id, resposta1, resposta2 e resposta3.

Pré-requisitos

pip install oci matplotlib numpy --quiet

O código

Setup

import oci
import csv
import io
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from collections import defaultdict

COMPARTMENT_ID = "ocid1.compartment.oc1..SEU_OCID_AQUI"
BUCKET_NAME    = "feedback-turmas"

config         = oci.config.from_file()
object_storage = oci.object_storage.ObjectStorageClient(config)

vision_client  = oci.ai_vision.AIServiceVisionClient(
    config,
    service_endpoint="https://vision.aiservice.us-chicago-1.oci.oraclecloud.com"
)

ai_client      = oci.ai_language.AIServiceLanguageClient(
    config,
    service_endpoint="https://language.aiservice.us-chicago-1.oci.oraclecloud.com"
)

NAMESPACE = object_storage.get_namespace().data

O NAMESPACE é do tenant, não da região. O mesmo valor serve para qualquer bucket da conta. Os endpoints usados aqui são de Chicago, onde Vision, Language e demais serviços de IA estão disponíveis. Para outras regiões, substituir us-chicago-1 pelo identificador correspondente.

Lendo os CSVs do Bucket

def ler_csv_do_bucket(object_name):
    obj     = object_storage.get_object(NAMESPACE, BUCKET_NAME, object_name)
    content = obj.data.content.decode('utf-8')
    return list(csv.DictReader(io.StringIO(content)))

Os arquivos ficam no bucket. O script puxa direto via SDK, sem download local.

OCR com OCI Vision

Com as imagens digitalizadas no bucket, o OCI Vision extrai os textos de cada uma delas e então são enviados ao OCI Language.

def extrair_texto_ocr(image_name):
    response = vision_client.analyze_image(
        analyze_image_details=oci.ai_vision.models.AnalyzeImageDetails(
            features=[
                oci.ai_vision.models.ImageTextDetectionFeature(
                    feature_type="TEXT_DETECTION"
                )
            ],
            image=oci.ai_vision.models.ObjectStorageImageDetails(
                source="OBJECT_STORAGE",
                namespace_name=NAMESPACE,
                bucket_name=BUCKET_NAME,
                object_name=image_name
            )
        )
    )

    if not response.data.image_text:
        return ""

    return '\n'.join([linha.text for linha in response.data.image_text.lines])

O Vision retorna coordenadas de cada palavra e scores de confiança por linha, mas para este caso, vamos usar apenas o texto extraído. O resultado segue para o Language como documento, no mesmo fluxo dos CSVs.

Análise de sentimento com OCI Language

Aqui um detalhe importante: durante os testes foi preciso omitir o parâmetro language_code. Ao omiti-lo, a detecção automática reconhece o idioma Pt-Br. Indicar "pt" retorna um erro 400.

O limite é de cem documentos por chamada, mas como temos trinta alunos com três respostas cada, nosso total chega em 90 documentos por turma, o que está dentro do limite da ferramenta.

def processar_turma(dados_csv):
    documentos = []
    for row in dados_csv:
        for i, col in enumerate(['resposta1', 'resposta2', 'resposta3'], 1):
            documentos.append(
                oci.ai_language.models.TextDocument(
                    key=f"{row['id']}_p{i}",
                    text=row[col]
                    # Não passar language_code aqui.
                    # Passar "pt" explicitamente retorna 400.
                )
            )

    response = ai_client.batch_detect_language_sentiments(
        batch_detect_language_sentiments_details=oci.ai_language.models.BatchDetectLanguageSentimentsDetails(
            documents=documentos,
            compartment_id=COMPARTMENT_ID
        )
    )
    return response.data.documents

def calcular_scores(documentos):
    scores = {f'p{i}': defaultdict(int) for i in range(1, 4)}
    for doc in documentos:
        pergunta = doc.key.split('_')[1]
        scores[pergunta][doc.document_sentiment] += 1
    return scores

Gráfico e Resultado enviados ao Bucket

O gráfico é gerado por Python com a biblioteca matplotlib, rodando diretamente no Cloud Shell. O Cloud Shell é um ambiente Linux gerenciado pela Oracle, acessível pelo console OCI sem nenhuma instalação local.

O matplotlib é instalado via pip no início do script e não precisa de interface gráfica para funcionar: a renderização usa o backend Agg, que gera o arquivo em memória sem precisar de display. O resultado vai direto para o bucket via Object Storage SDK.

O gráfico produzido tem esse formato, com três painéis, um por pergunta. Cada barra representa o número de respostas classificadas pelo OCI Language em quatro categorias: positiva, negativa, neutra e mista. A categoria mista aparece quando o texto contém sentimentos conflitantes na mesma resposta.

def gerar_e_salvar_grafico(resultados):
    turmas           = ['turma_a', 'turma_b', 'turma_c']
    perguntas_labels = ['Ano letivo', 'Relacao com professor', 'Aprendizado pessoal']
    pkeys            = ['p1', 'p2', 'p3']
    cores            = {
        'Positive': '#2ecc71',
        'Neutral':  '#95a5a6',
        'Negative': '#e74c3c',
        'Mixed':    '#f39c12'
    }

    fig, axes = plt.subplots(1, 3, figsize=(15, 5))

    for ax, label, pkey in zip(axes, perguntas_labels, pkeys):
        categorias = ['Positive', 'Neutral', 'Negative', 'Mixed']
        x = np.arange(len(turmas))
        w = 0.2
        for i, cat in enumerate(categorias):
            valores = [resultados[t][pkey].get(cat, 0) for t in turmas]
            ax.bar(x + i * w, valores, w, label=cat, color=cores[cat])
        ax.set_title(label)
        ax.set_xticks(x + w * 1.5)
        ax.set_xticklabels(['Turma A', 'Turma B', 'Turma C'])
        ax.set_ylabel('Respostas')
        ax.legend(fontsize=8)

    plt.suptitle('Sentimento por turma: OCI Language', fontsize=13)
    plt.tight_layout()

    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)

    object_storage.put_object(NAMESPACE, BUCKET_NAME, 'resultado_sentimento.png', buf)
    print("Grafico salvo no bucket: resultado_sentimento.png")

O gráfico não passa pelo disco do Cloud Shell. Ele é gerado em memória e vai direto para o bucket.

Executando

turmas     = ['turma_a', 'turma_b', 'turma_c']
resultados = {}

for turma in turmas:
    print(f"Processando {turma}...")
    dados = ler_csv_do_bucket(f"{turma}.csv")
    docs  = processar_turma(dados)
    resultados[turma] = calcular_scores(docs)

gerar_e_salvar_grafico(resultados)
python3 analise_sentimento.py
Processando turma_a...
Processando turma_b...
Processando turma_c...
Grafico salvo no bucket: resultado_sentimento.png

O script fica salvo no $HOME do Cloud Shell e o diretório persiste entre sessões.

O que os dados mostram

Nas respostas da Turma A, as perguntas dois e três andam juntas: quem avaliou negativamente a relação com o professor avaliou negativamente o próprio aprendizado. Cerca de 80% das respostas seguem esse padrão. A pergunta sobre o ano letivo em geral tem mais dispersão, o que indica que os alunos localizam o problema em vez de generalizá-lo.

A Turma B não tem um direcionamento claro. Positivos e negativos distribuídos sem nenhum padrão aparente entre as três perguntas. O tipo de resultado mais difícil de trabalhar.

Já na Turma C há um claro direcionamento da turma. Além da maioria das respostas serem positivas, a relação com o professor e a avaliação do próprio aprendizado andam juntas, com uma consistência de quase 80% que a Turma A mostrou, mas no sentido oposto. Com o gráfico, tudo isso fica nítido sem precisar ler 90 respostas.

Um professor sem acesso a esses dados poderia ler a Turma C como uma turma quieta, mas com esses dados, o quadro é diferente. No nosso caso, a turma C era composta de alunos também neurodivergentes.

O que realmente importa

Esse artigo não é sobre os resultados, até porque eles foram gerados sinteticamente apenas para ser possível criar essa situação exemplo. O importa aqui é a execução de um pipeline simples para análise de sentimento, aplicável em diversas situações, com pouca ou nenhuma adaptação. Desde empresas que precisam analisar milhares de feedback recebidos diariamente, como também o usuário final, especialmente aqueles que podem ter problemas com leitura de sinais sociais, em nosso caso, pessoas no espectro autista.

Como eu.

Leia mais

← Todos os artigos
editar