-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/embedding #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces an automatic labeling feature leveraging the OpenAI API to generate and refine cluster labels. Key changes include:
- The addition of utility functions for generating and refining cluster labels in utils_auto_label.py.
- Integration of the auto-labeling feature into the cluster module.
- An update to the Python version requirement and dependency revisions in pyproject.toml.
Reviewed Changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/cluster_facil/utils_auto_label.py | New utilities for generating and refining cluster labels using the OpenAI API. |
| src/cluster_facil/cluster.py | Added an auto_label_cluster method to integrate automatic labeling into the clustering workflow. |
| pyproject.toml | Updated the python version requirement and added/updated dependencies including openai and python-dotenv. |
Files not reviewed (1)
- .env.example: Language not supported
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Acho que uma boa ideia seria usar o langchain, porque ele tem muito mais suporte e funções que o llm do simonw. Única coisa meio chatinha é que ela envolveria incluir uma dependência a mais para cada provedor, o que acrescenta uma dificuldade adicional para quem não programa, mas quer utilizar a biblioteca. No entanto, como eles já vão ter que ir atrás de uma chave de API de qualquer jeito, não vejo grande problema nisso. |
bdcdo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Achei que a implementação ficou ótima. Vou depois testar pra ver como ficou. Estou bastante curioso pra ver se com esses prompts simples já funciona, ou se pra ficar algo menor precisamos construir um agente.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tava na minha lista de tarefas pensar em que versão de python vamos colocar como mínima. Imagino que trocou para 3.10 por conta do uso do list. O ideal é só identificar qual o menor valor que não quebra o nosso código, certo? Se sim, tudo certo.
| logging.info(f"Analisando características de {len(df_para_preparar)} textos (TF-IDF)...") | ||
| # Define parâmetros padrão que podem ser sobrescritos pelos kwargs | ||
| default_tfidf_params = {'stop_words': STOPWORDS_PT} | ||
| default_tfidf_params = {'stop_words': list(STOPWORDS_PT)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tive também problema com isso e acabei solucionando trocando no arquivo utils como o STOPWORDS_PT está definido. Troquei de tupla pra lista e já fiz o push pra main. Acho que podemos deixar essa redundância. O que acha?
| logging.info(f"Contagem de textos por classificação manual na coluna '{self.nome_coluna_classificacao}':\n{contagem}") | ||
| return None | ||
|
|
||
| def auto_label_cluster(self, rodada: int = None, model: str = "gpt-4.1-nano", api_key: str = None, temperature: float = 0.0, cut_limit: int = 30, random_state: int = None, final_refine: bool = True, n_examples_final: int = 10) -> dict: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
O random_state padrão tá como 42 em outros lugares. Acho que vale padronizar em um só valor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considerando que esse é um método de um objeto da classe ClusterFacil, acho que não precisa incluir "cluster" no nome.
Podemos também abrasileirar para "auto_classificar" ou algo do tipo.
| model (str): Nome do modelo OpenAI (default: 'gpt-4.1-nano'). | ||
| api_key (str, opcional): Chave da API OpenAI. Se não fornecida, busca em OPENAI_API_KEY. | ||
| temperature (float): Temperatura do modelo. | ||
| cut_limit (int, opcional): Número máximo de textos a serem enviados para o LLM por cluster. Se None, usa todos. Default=30. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acho que o default pode ser menor para economizar tokens. Estamos usando 10 para analisar manualmente e tem funcionado bem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Estou me referindo ao cut_limit
| api_key (str, opcional): Chave da API OpenAI. Se não fornecida, busca em OPENAI_API_KEY. | ||
| temperature (float): Temperatura do modelo. | ||
| cut_limit (int, opcional): Número máximo de textos a serem enviados para o LLM por cluster. Se None, usa todos. Default=30. | ||
| random_state (int, opcional): Semente para amostragem aleatória dos textos. Default=None (não controla aleatoriedade). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mencionei acima a sugestão de padronizar em 42.
Numa nota mais de desenho da biblioteca, acho que o ideal seria controlar todas as aleatoriedades possíveis para tornar os resultados de cada pesquisador reprodutíveis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E aqui ao random_state
| # Utiliza a Responses API mais recente com formato estruturado | ||
| resp_format = { | ||
| "format": { | ||
| "type": "json_schema", | ||
| "name": "rotulo", | ||
| "schema": { | ||
| "type": "object", | ||
| "properties": { | ||
| "rotulo": { | ||
| "type": "string", | ||
| "description": "Rótulo curto, claro e descritivo para o cluster de textos." | ||
| } | ||
| }, | ||
| "required": ["rotulo"], | ||
| "additionalProperties": False | ||
| }, | ||
| "strict": True | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Langchain nos ajudaria também a garantir que todos os provedores nos forneçam respostas parseadas em json
| if not api_key: | ||
| api_key = os.getenv("OPENAI_API_KEY") | ||
| if not api_key: | ||
| raise ValueError("É necessário fornecer uma chave de API OpenAI via argumento ou variável de ambiente OPENAI_API_KEY.") | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Se isso vai se repetir, acho que, na hora de generalizar para poder receber outras APIs, podemos criar uma função que lida com as chaves e a armazena junto ao cluster_facil.
| prompt = ( | ||
| "Você receberá exemplos de clusters, cada um com um rótulo sugerido e algumas amostras de textos.\n" | ||
| "Sua tarefa é:\n" | ||
| "- Unificar rótulos semelhantes se fizer sentido,\n" | ||
| "- Sugerir nomes mais claros e concisos para cada grupo,\n" | ||
| "- Retornar um dicionário JSON com o id do cluster e o novo rótulo.\n\n" | ||
| "Exemplo de entrada:\n" | ||
| "Cluster 0 - Rótulo inicial: 'Esportes'\n<sample1>Texto exemplo</sample1>\n<sample2>Texto exemplo</sample2>\n\n" | ||
| "Cluster 1 - Rótulo inicial: 'Futebol'\n<sample1>Texto exemplo</sample1>\n<sample2>Texto exemplo</sample2>\n\n" | ||
| "Agora, siga o mesmo padrão para os clusters abaixo:\n" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tenho a impressão de que para essa revisão um llm de raciocínio funcionaria melhor. Podemos testar ter como padrão o o4-mini-low.
| prompt = ( | ||
| "Dado o seguinte conjunto de textos, gere um rótulo curto (tema) que represente o cluster. " | ||
| "O rótulo deve ser claro, conciso e descritivo." | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| prompt = ( | |
| "Dado o seguinte conjunto de textos, gere um rótulo curto (tema) que represente o cluster. " | |
| "O rótulo deve ser claro, conciso e descritivo." | |
| ) | |
| prompt = ( | |
| "Dado o seguinte conjunto de textos, gere um rótulo curto (tema) que represente o cluster. " | |
| "O rótulo deve ser claro, conciso e descritivo.\n" | |
| "Se não for possível identificar um padrão claro entre as decisões, responda "falta_coesão". | |
| ) |
Uma parte importante das rodadas de clusterização é entender quais clusters estão coesos e quais não estão. Acho que precisamos indicar isso explicitamente como uma possibilidade, para que esses casos possam ser novamente clusterizados.
Nesse sentido, seria interessante eventualmente incluir a possibilidade de que uma segunda rodada de auto_label seja automaticamente aplicada apenas nos casos considerados não coesos. E, a médio prazo, poderiamos automatizar seguidas rodadas de reclusterização.
Ligado a isso, registro aqui algum ceticismo em relação a se modelos menores vão ser capazes de fazer essa decisão de falta de unidade. Acho que os modelos menores vão se sentir mais pressionados a dizer que há algo em comum, pelo que já tive de experiência em outros casos.
| for cid, info in cluster_samples.items(): | ||
| label = info["label"] | ||
| examples = "\n\n".join(f"<sample{i+1}>\n{t}\n</sample{i+1}>" for i, t in enumerate(info["examples"])) | ||
| clusters_str += f"Cluster {cid} - Rótulo inicial: '{label}'\n{examples}\n\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eu me sentiria confortável até em reduzir para metade os clusters que aparecem aqui.
Acho que vale incluir também algum tipo de conferência de tamanho dos tokens nessa chamada, para evitar passar do limite em casos limite.
Adiciona feature de auto label. Por enquanto apenas com OpenAI, mas seria legal usar alguma ferramenta mais geral para lidar com outros modelos
Por exemplo, usando o llm do simonw, que o guilherme recomendou:
Obs: não precisa dar merge ainda, vamos conversar antes