Processos e informações estão no centro de todos os negócios. A vulnerabilidade e a oportunidade deste momento é a questão de saber se sua empresa pode automatizar seus processos usando a IA e colher as recompensas de fazê -lo. O Chatgpt, uma IA de uso geral, abriu nossos olhos para o que a IA pode fazer. O que importa agora está direcionando o poder da IA para os seus problemas de negócios e desbloqueando o valor de seus dados proprietários. Neste documento, mostrarei como.
Você não quer uma IA que possa conversar; O que você realmente deseja são automações que executam o trabalho que mantém seus negócios funcionando-potência através de processos de negócios com grande precisão e escala. A maneira comprovada de personalizar a IA para seus processos de negócios é ajustar um LLM nos seus dados e na ação que você deseja que a IA execute.
Vamos falar especificamente sobre o ajuste fino que faremos neste documento e na tecnologia por trás dele. Listados abaixo estão as cinco ferramentas que usaremos extensivamente:
No final do dia, você deve tirar duas coisas deste documento:
Vou descrever para você um problema duro e no mundo real, o que os pesquisadores de aprendizado de máquina já fizeram e a nova fronteira que conseguimos pressionar a usar ferramentas poderosas e SOTA. Vamos treinar modelos nas GPUs na nuvem. Também vamos colocar em ação poderosas práticas de MLOPs-usando o MLFlow para organizar nossos experimentos e parâmetros. E ao longo do caminho, vou apontar o padrão de design deste projeto para que você possa personalizar a base de código para seus próprios projetos de aprendizado profundo.
Vamos começar com o plano de fundo do problema.
Um bom processo para encontrar problemas adequados para o aprendizado de máquina e para conjuntos de dados de qualidade é começar navegando sites com benchmarks. Os benchmarks fornecem um quadro de referência para o nível de dificuldade do problema para o aprendizado de máquina, que usamos para medir nosso progresso durante o desenvolvimento do modelo. Um conjunto de dados específico com benchmarks bem estabelecidos é o conjunto de dados injustos de termos de serviço (desleal-tos); Aqui está uma declaração intrigante de problema: use a IA para encontrar todas as cláusulas injustas em termos de contratos de serviço. O contexto é que a lei européia do consumidor em contratos injustos estabelece o que são cláusulas injustas e os diferentes tipos de cláusulas injustas. O que torna os TOs injustos perfeitos para a classificação de texto é que ela foi rotulada manualmente de acordo com a que foi estabelecida na lei européia.
Chalkidis et al. (2021) aplicaram oito métodos diferentes de aprendizado de máquina a TOs injustos e obtiveram a macro F1 variando de 75 a 83 e, na Figura 1, logo abaixo, extraímos seus resultados publicados.
| Método | Tos injustos | |
|---|---|---|
| Micro F1 | Macro F1 | |
| Tfidf-svm | 94.7 | 75.0 |
| Bert | 95.6 | 81.3 |
| Roberta | 95.2 | 79.2 |
| Deberta | 95.5 | 80.3 |
| Longformer | 95.5 | 80.9 |
| BigBird | 95.7 | 81.3 |
| Legal-Bert | 96.0 | 83.0 |
| Caselaw-Bert | 96.0 | 82.3 |
Coisas interessantes que podemos inferir desta tabela são:
Olhando para os dados, o desequilíbrio de classe está certamente presente, o que é um bom motivo para o primeiro e o segundo ponto acima.
Existem oito tipos diferentes de cláusulas injustas. Os autores desse artigo desenvolveram modelos de classificação de vários rótulos para os oito tipos, mas simplesmente vamos construir um modelo de classificação binária que classifica uma cláusula como justa ou injusta.
Vamos projetar como vamos fazer isso.
__init__.py expõe as APIs públicas do módulo em que se encontra, para que possamos reduzir convenientemente as importações para funcionalidade profundamente submodulada como este exemplo: from . fine_tune_clsify_head import TransformerModule A importação acima nas architectures/__init__.py permite que o código externo architectures importem TransformerModule sem precisar se lembrar das migalhas que levam a onde essa função está, como esta:
from architectures import TransformerModuleAqui está a aparência da estrutura do projeto da raiz do módulo Lightning_MLFlow:
.
├── architectures
│ ├── __init__.py
│ └── fine_tune_clsify_head.py
├── config
│ ├── __init__.py
│ └── train_config.py
├── data
│ ├── __init__.py
│ └── lex_glue.py
└── train.py
A questão principal é por que o projeto está estruturado dessa maneira. E a resposta é que a separação de preocupações é estabelecida entre os 3 submódulos e 1 ponto de entrada. Vamos atravessar cada um desses módulos, por sua vez:
architectures : Especifica essencialmente tudo o que Pytorch precisa saber, exceto os dadosconfig : mantém todos os parâmetros e pode ser subdividido em train_config.py e inference_config.py e qualquer outra coisadata : contém toda a lógica de processamento de dados e configuraçõestrain.py : O ponto de controle central; Este é o único pedaço de código que pode ver architectures , config e data ; MLFlow SDK Chamadas vá aquiCom esta breve introdução fora do caminho, vamos arregaçar as mangas e tentar realmente entender o código em cada um dos módulos. Vamos começar com arquiteturas/fine_tune_clsify_head.py. Este código está organizado como a Figura 3 logo abaixo.

É claro que os métodos principais dentro desta classe para entender são: o construtor, a passagem para a frente pela rede, as métricas e as etapas. Discutiremos cada um deles por sua vez. Vamos começar com o construtor. Este trecho é como é o código:
O que é realmente crucial aqui é que o construtor carrega um modelo pré -terenciado de abraçar o rosto e configura o parâmetro Tuning Fine (PEFT). O PEFT é a maneira de borda de sangramento para ajustar modelos de linguagem grande e é uma implementação de qlora, que é a combinação de três estratégias de eficiência diferentes: quantização, baixa classificação e adaptador. A quantização é a idéia de que um número inteiro de bytes é de precisão numérica suficiente para cálculos de gradiente no aprendizado profundo. A baixa classificação vem do conceito de álgebra linear de que a classificação de uma matriz é o número de dimensões não redundantes do espaço vetorial. O ajuste do adaptador pode ser entendido como uma espécie de generalização de ajustar a cabeça da rede, reformando uma parte dos pesos em muitas camadas.
Vejamos o método forward :
Como o treinamento de uma rede neural envolve a alternância entre o BackProp e o Feed Forward, isso especifica a parte avançada, e o que é notável é que os argumentos do forward são: input_ids , attention_mask e label . Essas são as saídas de um tokenizador de rosto abraçando e nos permitem amarrar abraçando o rosto e o raio Pytorch.
Em seguida, vamos dar uma olhada no método _compute_metrics :
Há algumas coisas que valem a pena notar aqui. Primeiro, observe como as logits são descompactadas da avaliação da função direta e depois convertidas em probabilidades por meio da função Softmax. Isso, por sua vez, leva à previsão binária. Usamos a previsão e o real ( batch["label"] ) para calcular a precisão, F1, precisão e recall.
Por fim, vamos falar sobre training_step , validation_step e test_step .
Eles parecem quase iguais, com uma diferença importante sendo training_step retornando outputs["loss"] em vez de metrics . Isso faz sentido porque o backprop está minimizando iterativamente a perda; training_step é apenas a abstração do Lightning do Loop de Treinamento por Pytorch Lightning.
Ok, agora terminamos com architectures , vamos mergulhar no config/tren_config.py, que se parece com a Figura 7 abaixo.
Esse código é auto-explicativo, mas há algumas opções para chamar. O pretrained_model está definido como roberta-base e, se a trocávamos para qualquer nome de modelo de rosto abraçado que suporta o AutoTokenizer, tudo ainda deve funcionar (testado). A precisão do modelo foi considerada mais sensível ao max_length e batch_size ; portanto, esses são os principais hiperparâmetros para brincar, e ambos trocam entre precisão e carga computacional. No Azure ML, usei a instância padrão_NC24ADS_A100_V4. O VRAM na GPU A100 provou ser o fator limitante para esses dois hiperparâmetros. O que eu descobri foi que 256 foi o maior batch_size que não faria com que Cuda apresentasse um erro fora da memória.
Determinar max_length de 128 foi mais fácil, pois a grande maioria das cláusulas estava sob esse limite de token.
Finalmente, como veremos mais adiante, max_epochs de 10 foi escolhido por tentativa e erro como a métrica de validação F1 que estávamos rastreando no mlflow estava achatando nesse ponto.
Ok, vamos falar sobre dados. Tudo o que você precisa saber está na classe LexGlueDataModule em dados/lex_glue.py, e essa é sua estrutura:
Existem alguns detalhes de êxtase da questão no LexGlueDataModule , mas a idéia principal é que o método setup busque os dados diretamente da face abraçada e, em seguida, algum tipo de tokenização deve acontecer. Os detalhes dessa tokenização estão no método _shared_transform , que é apenas a interface de alto nível para o método de retorno de chamada _preprocess .
Finalmente, estamos prontos para o trem.py, o ponto de controle central de tudo. Lembra quando começamos esta seção com a estrutura do diretório do projeto? Essa estrutura está comunicando uma mensagem importante: que as dependências são resolvidas para baixo na árvore do diretório. Em outras palavras, restringimos o código a chamar apenas outro código que está no mesmo nível ou menor. Essa restrição certamente não vem do Python; É auto-imposto e por quê? Para eliminar adivinhação. Não há dúvida de que as cadeias de dependência confusas são um grande problema em grandes bases de código. O que tudo isso significa é que o train.py . Vejamos o pedaço de código no coração:
Estamos instantando uma aula de treinador Pytorch Lightning, ativando a parada precoce e definindo a precisão com base na máquina em que estamos no via precision="bf16-mixed" if torch. cuda.is_available() else "32-true" . Configuramos o MLFlow para rastrear tudo. Para reunir tudo, importamos a funcionalidade dos outros três módulos que descrevemos anteriormente:
from architectures . fine_tune_clsify_head import TransformerModule
from config import TrainConfig
from data import LexGlueDataModuleAs funcionalidades do núcleo são envolvidas em nossas subclasses de superclasses de Pytorch Lightning, e assim as instanciamos:
model = TransformerModule (
pretrained_model = config . pretrained_model , num_classes = config . num_classes ,
lr = config . lr , )
datamodule = LexGlueDataModule (
pretrained_model = config . pretrained_model , max_length = config . max_length ,
batch_size = config . batch_size , num_workers = config . num_workers ,
debug_mode_sample = config . debug_mode_sample , ) E, finalmente, invocamos os métodos de fit e test do objeto trainer :
trainer . fit ( model = model , datamodule = datamodule )
best_model_path = checkpoint_callback . best_model_path
# Evaluate the last and the best models on the test sample.
trainer . test ( model = model , datamodule = datamodule )
trainer . test (
model = model , datamodule = datamodule , ckpt_path = best_model_path , ) Tínhamos chamado test duas vezes para que estejamos testando o último modelo no horário de parada e o melhor modelo de acordo com nossa métrica. Este é um exemplo perfeito da rica funcionalidade que vem com o Pytorch Lightning.
E isso encerra o passo a passo. Agora vamos mergulhar nos resultados do modelo.
Aqui está um painel que junta tudo:
Aproveitando a forte integração entre o Azure e o MLFlow, as métricas de nossa execução de treinamento foram disponibilizadas para fácil consumo e visualização no console da Web do Azure ML. O MLFlow capturou muito mais do que apenas as métricas, mas aprofundar sua funcionalidade total e os casos de uso mais amplos de MLOPs são para outro dia. Por enquanto, vamos nos concentrar na história que nossa visualização de dados nos revela.
Uma coisa que sai do gráfico superior esquerdo é que o treinamento sem dúvida melhorou a perda na amostra de validação. Além disso, entre 125 e 190 passos, a curva começa a achatar, uma indicação potencial de que um treinamento adicional pode ser desnecessário. Mas, sendo monotônico, vemos que o excesso de ajuste ainda não ocorreu e, além do mais, a curva se afastou de 190 para 210, então talvez devêssemos deixar isso correr por mais 5 épocas.
Curiosamente, o enredo superior direito contam uma história semelhante. A característica mais notável é o quão alto os valores de precisão são, e a unidade aqui é porcentagem. O fato de as linhas estarem atingindo 90% desde o início, porque o conjunto de dados está desequilibrado na proporção de 1: 9 entre injusto e justo. É exatamente por isso que a métrica preferida é F1, que é o que exibimos no gráfico inferior esquerdo.
As curvas F1 aqui exibem o padrão clássico em que o modelo melhora rapidamente no início, mas acaba chega ao ponto de retorno decrescente. Uma pergunta interessante provocada pelas curvas de F1 é como nos reconciliamos entre a perda e a F1, quando um sugere diminuindo enquanto o outro sai de espaço para ir mais longe? A seguinte parte da teoria quebrará esse enigma.
Observamos o mundo na forma de eventos discretos, mas construções contínuas (por exemplo, probabilidade) geralmente são mais úteis em matemática. Os resultados no conjunto de dados de TOS desleais são codificados "justos" ou "injustos", e há uma distribuição de probabilidade latente que gerou esses resultados. Essa distribuição de probabilidade latente é com o que a função de perda de entropia cruzada pode funcionar. Portanto, no aprendizado profundo, otimizamos a métrica contínua, mas julgamos o quão bom é o nosso modelo pela métrica discreta, porque contínuo (latente) e discreto (observado) são aproximações um do outro. Essa é a ideia.
Portanto, quando o F1 sugere uma história diferente da perda, existem apenas duas explicações possíveis: aleatoriedade e desequilíbrio de classe. De fato, é por isso que a precisão e a F1 são muito mais voláteis que a perda.
A aleatoriedade também parece ser a explicação provável para a lacuna entre a melhor pontuação F1 de validação de 70% e a pontuação de F1 de teste de 78% (da tabela inferior direita).
Esta reprodução por análise de reprodução das métricas de avaliação do modelo termina esta seção. Em seguida, consiga fazer com que este projeto seja o seu próprio. O guia de instalação abaixo ajudará você a começar a mexer com o código.
Etapa 1. Clone este repositório em um diretório de trabalho local:
$ git clone https://github.com/zjohn77/lightning-mlflow-hf.git
$ cd lightning-mlflow-hf Etapa 2. Todas as dependências do projeto estão no environment.yml e você criará um ambiente virtual para isso. As instruções abaixo são para conda , mas nada nessas dependências excluem venv ou poetry .
$ conda env create -n lightning-mlflow-hf -f environment.yml Etapa 3. No final da execução do treinamento, a função copy_dir_to_abs copiará as saídas para o Azure Blob Storage. Se o Azure também é o que você está usando, basta passar credenciais para essa função e você está pronto. Caso contrário, substitua seu próprio fluxo de trabalho.
Etapa 4. Em seu ambiente virtual, você mudará ou apontará o seu IDE para onde train.py está e executa:
$ python train.pySe não houver bugs, ele imprimirá para consolar uma mensagem de rosto abraçando e muitas mensagens de Lightning Pytorch. Depois de alguns minutos, deve começar a imprimir uma barra de progresso. Sente -se bem e deixe fazer isso. Quando a execução finalmente terminar, uma tabela ASCII resumindo as métricas de avaliação para o modelo final será impressa para consolar. Isso é tudo o que há para isso!
Qualquer contribuição é bem -vinda. Utilizamos solicitações do Github Pull para revisão de código e usamos o Formatter Black para garantir a consistência do estilo de código.
Os testes de unidade e os testes de DOC também são muito bem -vindos.
Um roteiro é um trabalho em andamento. Volte em breve.
Ilias Chalkidis, Abhik Jana, Dirk Hartung, Michael Bommarito, Ion Androutsopoulos, Daniel Martin Katz e Nikolaos Aletras. (2021). LexGlue: Um conjunto de dados de referência para o entendimento da linguagem legal em inglês . Retirado de Arxiv: https://arxiv.org/abs/2110.00976