Messa a punto di LoRA con Granite LLM

Ottimizzazione di Granite con LoRA

Il Low-Rank Adaptation (LoRA) è un metodo efficiente di messa a punto che riduce il numero di parametri addestrabili che aumenta la velocità e l'utilizzo delle risorse della formazione mantenendo la stessa qualità degli output. Invece di aggiornare tutti i parametri in una [rete neurale] (https://www.ibm.com/it-it/think/topics/neural-networks) durante la messa a punto, LoRa blocca i pesi pre-addestrati originali e aggiunge matrici low-rank piccole e addestrabili che approssimano le modifiche necessarie per la nuova attività. Questo approccio si basa sull'ipotesi che gli aggiornamenti di peso durante l'adattamento abbiano un "rango intrinseco basso".

Un vantaggio aggiuntivo di LoRa è che, poiché i pesi pre-addestrati vengono mantenuti congelati, l'adattatore generato è leggero e portatile e può essere facilmente memorizzato.

In questo tutorial utilizzerai LLaMa Factory. LLaMa Factory è una piattaforma di formazione e ottimizzazione low e no-code di modelli linguistici di grandi dimensioni (LLM) che consente agli utenti di adattare gli LLM su set di dati personalizzati, valutare le prestazioni e fornire modelli. Ha un'interfaccia utente web e una CLI facili da usare e supporta oltre 100 LLM. La piattaforma supporta set di dati nei formati Alpaca e ShareGPT. LLaMa Factory non è l'unico modo per mettere a punto gli LLM: il PEFT la libreria per la messa a punto efficiente dei parametri è un'altra opzione per l'aggiornamento dei modelli di grandi dimensioni. PEFT offre la possibilità di eseguire LoRA quantizzato (QLoRA) per compattare ulteriormente il modello messo a punto. In questo tutorial, utilizzeremo una versione non quantizzata di Granite 3.3.

Sebbene LLama Factory possa funzionare senza l'uso di ampie risorse di calcolo, richiede una GPU e notevoli risorse di memoria. In questo tutorial, utilizzerai LLaMa Factory su watsonx per fornire risorse GPU e storage per l'adattatore generato.

Configurazione

Configurazione di Watson Studio

a. Accedi a watsonx.ai utilizzando il tuo account IBM® Cloud.

b. Crea un progetto watsonx.ai. Prendi nota dell’ID progetto nel progetto > Gestisci > Generali > ID progetto.  
Questo ID sarà necessario per questo tutorial.

c. Crea un’istanza di watsonx.ai Runtime Per questo tutorial, dovrai creare un’istanza a pagamento per accedere a una GPU.

d. Genera un’application programming interface watsonx (chiave API).

e. Associa il servizio watsonx.ai Runtime al progetto creato in watsonx.ai.

Cloud Object Storage

a. Per creare Cloud Object Storage per il tuo notebook, vai su https://cloud.ibm.com/ e seleziona “Crea istanza”.

b. Verrai indirizzato a una finestra di dialogo di creazione in cui potrai selezionare un listino prezzi. Per questo tutorial, basterà un piano standard.

c. Assegna un nome all’istanza di Cloud Object Storage.

d. Dopo aver creato la sua istanza, torna al Progetto e seleziona “ Nuovo asset “, quindi seleziona “Connetti a una fonte di dati”.

Immagine che mostra la connessione dati watsonx per Cloud Object Storage Configurare la connessione dati per Cloud Object Storage

e. Seleziona "Cloud Object Storage"

f. Nella finestra di dialogo successiva, seleziona l'istanza creata nei passaggi ad in base al nome.

g. Seleziona "Crea".

Crea un Jupyter Notebook

Crea un Jupyter Notebook.

a. Seleziona la scheda Asset nell'ambiente del progetto.

b. Fai clic su Nuova risorsa.

c. Seleziona l'opzione Lavorare con i modelli nel pannello di sinistra.

d. Fai clic su Lavorare con dati e modelli utilizzando notebook Python e R.

e. Inserisci un nome per il tuo notebook nel campo Nome. Selezionare tempo di esecuzione 23.1 su Python (4 vCPU 16 GB RAM) per definire la configurazione.

f. Seleziona Crea.

Configurazione

Successivamente, installerai le dipendenze sul tempo di esecuzione. Prima, llama-factory per generare gli adattatori di basso rango, poi Pandas per formattare il set di dati in formato Alpaca.

!pip install -q llamafactory 2>/dev/null
# pandas needed to format the dataset
!pip install -q --upgrade pandas 2>/dev/null

Controlla l'ambiente GPU

A seguire, dovrai assicurarti che il tuo ambiente watsonx abbia fornito una GPU compatibile con Torch che sarà necessaria per utilizzare LLaMa-Factory.

import torch

try:
  assert torch.cuda.is_available() is True
except AssertionError:
  print("No GPU found, please set up a GPU before using LLaMA Factory.")

Se lo snippet di codice precedente non stampa "Nessuna GPU trovata", allora sei a posto.

A seguire, importa librerie per manipolare i dati e per creare il file di configurazione LLaMa Factory usato per l'addestramento.

# Import libraries
import pandas as pd
import json
import yaml

Scarica ed elabora il set di dati MedReason

In questo tutorial, userai una parte del set di dati MedReason. MedReason è un set di dati di ragionamento medico di alta qualità e su larga scala, progettato per consentire una risoluzione spiegabile dei problemi medici negli LLM. Mentre MedReason si concentra sul ragionamento di un modello e sulla validazione delle catene di pensiero che un modello utilizza, in questo caso è anche utile fornire un set di dati troppo recente per essere incluso nei dati di addestramento di IBM® Granite 3.3.

Granite 3.3 è stato progettato per imparare attraverso la messa a punto, che verrà eseguita con LLaMa Factory. I modelli Granite possono essere ottimizzati in modo efficiente anche con risorse di calcolo limitate.

Carica una selezione del set di dati MedReason da GitHub:

from datasets import load_dataset

training = pd.read_json("https://raw.githubusercontent.com/UCSC-VLAA/MedReason/refs/heads/main/eval_data/medbullets_op4.jsonl", lines=True)

LLaMa Factory richiede che il set di dati sia preformattato in formati Alpaca o ShareGPT. Pertanto, riformattiamo i campi di domande e risposte del set di dati legale originale per contenere i campi di istruzione, input e output secondo il formato Alpaca.

Alpaca è un formato JSON per rappresentare un'istruzione, un input dell'utente e un output di sistema in questo modo:

{
    "instruction": "user instruction (required)",
    "input": "user input (optional)",
    "output": "model response (required)",
    "system": "system prompt (optional)",
}

Poiché MedReason non è formattato in Alpaca, crea un set di dati Alpaca nella cella successiva:

!mkdir -p data

# Format Med Dataset to Alpaca Format
formatted_data = [
    {
        "instruction": row["question"] + str(row["options"]),
        "input": "",
        "output": row["answer"]
    }
    for _, row in training.iterrows()
]

# output formatted MedReason dataset
with open("data/med.json", "w", encoding="utf-8") as f:
  json.dump(formatted_data, f, indent=2, ensure_ascii=False)

Llama Factory usa un file specifico per capire come caricare set di dati per l'addestramento. Questo file deve esistere nel percorso data/dataset_info.json. Quindi, dobbiamo creare un file data/dataset_info.json che include il percorso al nuovo set di dati medici formattato che abbiamo creato affinché la CLI di Llama Factory possa accedere al set di dati. Per i dettagli sul file dataset_info.json vedi la documentazione. All'interno del repository Llama Factory ci sono set di dati disponibili, tuttavia, poiché stiamo usando un nostro set di dati personalizzato, dobbiamo aggiungere il nostro set di dati al file JSON.

# "med" will be the identifier for the dataset 
# which points to the local file that contains the dataset
dataset_info = {
  "med": {
    "file_name": "med.json",
  }
}

# Create dataset_info.json with legal dataset so can reference with llama factory
with open("data/dataset_info.json", "w", encoding="utf-8") as f:
  json.dump(dataset_info, f, indent=2, ensure_ascii=False)

Ora che l'oggetto JSON formattato Alpaca è stato salvato nell'ambiente, sei pronto per iniziare l'addestramento.

Messa a punto

Il prossimo passo è impostare le configurazioni di formazione e poi scrivere le configurazioni in un file YAML che LLaMa-Factory utilizzerà per eseguire l'addestramento.

Ora esegui la messa a punto supervisionata (SFT) sul sottoinsieme del set di dati MedReason. LLaMa Factory supporta diversi tipi di addestramento. Alcuni dei più comunemente usati sono:

  • Pre-addestramento: quando un modello viene sottoposto a un addestramento iniziale utilizzando un set di dati ampio per generare risposte al linguaggio e alle idee fondamentali.

  • Ottimizzazione supervisionata (SFT): quando un modello riceve una formazione aggiuntiva con dati annotati per migliorare la precisione per una particolare funzione o su un argomento specifico.

  • Modellazione delle ricompense: laddove il modello acquisisce conoscenze su come ottenere uno specifico incentivo o ricompensa che influirà sull'ottimizzazione della politica prossimale (PPO) del suo output.

  • Addestramento: una tecnica di rapprendimento per rinforzo (RL) in cui il modello viene ulteriormente affinato attraverso tecniche di gradiente politico per aumentarne l'efficacia in un contesto specifico.

Esistono numerose impostazioni utilizzate per configurare LoRA, ma alcune delle più importanti e comunemente utilizzate sono:

  • Tasso di apprendimento (LR): il tasso di apprendimento determina la significatività con cui ciascun parametro del modello viene aggiornato durante ogni iterazione di addestramento. Un LR più alto può accelerare la convergenza consentendo aggiornamenti più grandi, ma rischia di superare la soluzione ottimale o di oscillare intorno ad essa. Un LR più basso porta a una convergenza più lenta ma più stabile, riducendo il rischio di instabilità vicino alla soluzione ottimale.

  • loraplus_lr_ratio: questo passaggio imposta il rapporto tra i tassi di apprendimento. In genere, dovrebbe essere > 1, ma la scelta ottimale di loraplus_lr_ratio dipende dal modello e dall'attività. Come linea guida, loraplus_lr_ratio dovrebbe essere più grande quando l'attività è più difficile e il modello deve aggiornare le sue caratteristiche per imparare bene. In questo caso, aiuta a ridurre leggermente il tasso di apprendimento (ad esempio, di un fattore 2) rispetto ai tipici tassi di apprendimento LoRa.

  • **Dimensione effettiva del batch **: configurare correttamente la dimensione del batch è fondamentale per bilanciare la stabilità dell'allenamento con le limitazioni della VRAM della GPU che sta utilizzando. La dimensione effettiva del batch è impostata dal prodotto di per_device_train_batch_size * gradient_accumulation_steps. In genere, maggiore è la dimensione effettiva del batch, più fluido e stabile è l'addestramento, ma potrebbe anche richiedere più VRAM di quella contenuta nella GPU. Una dimensione effettiva del batch più piccola può introdurre una maggiore varianza.

Ecco il codice che configura l'addestramento:

# setup training configurations
args = dict(
  stage="sft",  # do supervised fine-tuning
  do_train=True,  # we're actually training
  model_name_or_path="ibm-granite/granite-3.3-2b-instruct",  # use IBM Granite 3.3 2b instruct model
  dataset="med",  # use medical datasets we created
  template="granite3",   # use granite3 prompt template
  finetuning_type="lora", # use LoRA adapters to save memory
  lora_target="all",  # attach LoRA adapters to all linear layers
  loraplus_lr_ratio=16.0,  # use LoRA+ algorithm with lambda=16.0
  output_dir="granite3_lora",  # the path to save LoRA adapters
  per_device_train_batch_size=4,  # the batch size
  gradient_accumulation_steps=2,  # the gradient accumulation steps
  learning_rate=1e-4,  # the learning rate
  num_train_epochs=3.0, # the epochs of training
  max_samples=500,  # use 500 examples in each dataset
  fp16=True,  # use float16 mixed precision training
  report_to="none", # disable wandb logging
)

# create training config file to run with llama factory
with open("train_granite3_lora_med.yaml", "w", encoding="utf-8") as file:
  yaml.dump(args, file, indent=2)

La cella successiva addestrerà il modello e può impiegare fino a 10 minuti per essere eseguita:

!llamafactory-cli train train_granite3_lora_med.yaml;

Utilizzo di Cloud Object Storage

A seguire, crea due metodi per caricare e scaricare dati da IBM Cloud Object Storage:

from ibm_botocore.client import Config
import ibm_boto3

def upload_file_cos(credentials, local_file_name, key):  
    cos = ibm_boto3.client(service_name='s3',
    ibm_api_key_id=credentials['IBM_API_KEY_ID'],
    ibm_service_instance_id=credentials['IAM_SERVICE_ID'],
    ibm_auth_endpoint=credentials['IBM_AUTH_ENDPOINT'],
    config=Config(signature_version='oauth'),
    endpoint_url=credentials['ENDPOINT'])
    try:
        res=cos.upload_file(Filename=local_file_name, Bucket=credentials['BUCKET'],Key=key)
    except Exception as e:
        print(Exception, e)
    else:
        print(' File Uploaded')


def download_file_cos(credentials,local_file_name,key):  
    cos = ibm_boto3.client(service_name='s3',
    ibm_api_key_id=credentials['IBM_API_KEY_ID'],
    ibm_service_instance_id=credentials['IAM_SERVICE_ID'],
    ibm_auth_endpoint=credentials['IBM_AUTH_ENDPOINT'],
    config=Config(signature_version='oauth'),
    endpoint_url=credentials['ENDPOINT'])
    try:
        res=cos.download_file(Bucket=credentials['BUCKET'],Key=key,Filename=local_file_name)
    except Exception as e:
        print(Exception, e)
    else:
        print('File Downloaded')

La cella successiva contiene le credenziali di Cloud Object Storage.

Nel tuo notebook, clicca sulla scheda Snippet di codice nell'angolo destro. Questo passaggio apre un menu con diverse opzioni per i frammenti di codice generati. Seleziona "Leggi dati":

La finestra di dialogo per l'utilizzo di uno snippet di codice in Watson Studio Utilizzando uno snippet di codice preparato in Watson Studio

Questo passaggio apre un menu per selezionare un file di dati. Se non hai caricato nulla sulla tua istanza di Cloud Object Storage, dovrai caricare qualcosa per generare le credenziali. Può essere un set di dati classico come wine.csv.

Scegliere un asset in Watson Studio Scegliere un asset in Watson Studio

Dopo aver fatto clic su "Seleziona", puoi generare lo snippet delle credenziali con l'opzione "Carica come". Scegli "Inserisci codice nella cella":

Inserire di uno snippet di codice generato in Watson Studio Inserire di uno snippet di codice generato in Watson Studio

Questo passaggio genera una cella simile alla seguente con credenziali contenenti gli ID corretti e gli endpoint generati:

# @hidden_cell
# The following code contains metadata for a file in your project storage.
# You might want to remove secret properties before you share your notebook.

storage_metadata = {
    'IAM_SERVICE_ID': '',
    'IBM_API_KEY_ID': '',
    'ENDPOINT': '',
    'IBM_AUTH_ENDPOINT': '',
    'BUCKET': '',
    'FILE': ''
}

Ora la cartella zip contenente l'adattatore e le informazioni sull'adattatore stesso:

!zip -r "granite3_lora.zip" "granite3_lora"

Controlla di aver creato correttamente lo zip:

!ls

Inferenza

Ora è il momento di eseguire l'inferenza. L'inferenza sarà supportata dalla generazione di HuggingFace, che fornisce un metodo model.generate() per la generazione di testo utilizzando PyTorch.

Questo tutorial mostra porre al modello base una domanda medica tratta dal set di dati MedReason. È ragionevole pensare che il modello base non sia in grado di rispondere a questa domanda perché è un modello a uso generale addestrato su grandi e diversificati set di dati.

Per prima cosa, imposta le configurazioni di inferenza:

# setup inference configurations
args = dict(
  model_name_or_path="ibm-granite/granite-3.3-2b-instruct",  # use IBM Granite 3.3 2b instruct model
  template="granite3",  # set to the same one used in training, template for constructing prompts
  infer_backend="huggingface"  # choices: [huggingface, vllm]
)

# create inference config file to run with llama factory
with open("inference_config.yaml", "w", encoding="utf-8") as file:
  yaml.dump(args, file, indent=2)

Ora poni al chatbot una delle domande del set di dati MedReason:

from llamafactory.chat import ChatModel
chat_model = ChatModel(args)
messages = []

# run inference chatbot
question = '''
A 1-year-old girl is brought to a neurologist due to increasing seizure frequency over the past 2 months. 
She recently underwent a neurology evaluation which revealed hypsarrhythmia on electroencephalography (EEG) with a mix of slow waves, multifocal spikes, and asynchrony. 
Her parents have noticed the patient occasionally stiffens and spreads her arms at home. She was born at 38-weeks gestational age without complications. 
She has no other medical problems. Her medications consist of lamotrigine and valproic acid. Her temperature is 98.3\u00b0F (36.8\u00b0C), blood pressure is 90/75 mmHg, pulse is 94/min, and respirations are 22/min. 
Physical exam reveals innumerable hypopigmented macules on the skin and an irregularly shaped, thickened, and elevated plaque on the lower back. 
Which of the following is most strongly associated with this patient's condition?"
"A": "Cardiac rhabdomyoma", "B": "Glaucoma", "C": "Optic glioma", "D": "Polyostotic fibrous dysplasia"
'''

Crea un nuovo messaggio usando la domanda e passalo al modello base:

messages.append({"role": "user", "content": question})

response = ""
for new_text in chat_model.stream_chat(messages):
    response += new_text

print(response)
messages.append({"role": "assistant", "content": response})

Ecco l'output campione del modello base Granite 3.3

Utente:

A 1-year-old girl is brought to a neurologist due to increasing seizure frequency over the past 2 months. 
She recently underwent a neurology evaluation which revealed hypsarrhythmia on electroencephalography (EEG) with a mix of slow waves, multifocal spikes, and asynchrony. 
Her parents have noticed the patient occasionally stiffens and spreads her arms at home. She was born at 38-weeks gestational age without complications. 
She has no other medical problems. Her medications consist of lamotrigine and valproic acid. Her temperature is 98.3\F (36.8\C), blood pressure is 90/75 mmHg, pulse is 94/min, and respirations are 22/min. 
Physical exam reveals innumerable hypopigmented macules on the skin and an irregularly shaped, thickened, and elevated plaque on the lower back. 
Which of the following is most strongly associated with this patient's condition?"
"A": "Cardiac rhabdomyoma", "B": "Glaucoma", "C": "Optic glioma", "D": "Polyostotic fibrous dysplasia"

Risposta:

The most strongly associated condition with this patient's condition is "C": "Optic glioma".

The patient's symptoms of hypsarrhythmia on EEG, seizure frequency increase, and the presence of hypopigmented macules and a thickened plaque on the lower back are indicative of a neurological disorder. Optic glioma is a type of brain tumor that can present with these symptoms, including seizures and visual disturbances.

Option A, "Cardiac rhabdomyoma", typically presents with cardiac involvement and is not associated with the described EEG findings or skin manifestations.

Option B, "Glaucoma", is an eye disease that can lead to vision loss but is not associated with the EEG findings or skin lesions described.

Option D, "Polyostotic fibrous dysplasia", is a bone disorder characterized by multiple bone lesions and is not associated with the neurological symptoms and EEG findings presented.

Therefore, based on the clinical presentation, the most likely diagnosis is an optic glioma.

La risposta corretta dal set di dati è:

answer: Cardiac rhabdomyoma

Quindi il modello base non ha generato la risposta corretta.

Inferenza con l'adattatore ottimizzato LoRA

Confrontiamo i risultati con il modello di base e l'adattatore ottimizzato LoRA. Poi poniamo la stessa domanda per vedere come la sintonizzazione con il set di dati legali ha permesso al modello di comprendere meglio e rispondere alle domande mediche.

La seguente cella non sarà necessaria se hai eseguito LoRA nella stessa sessione. Tuttavia, se stai tornando al Jupyter Notebook e non vuoi riaddestrare, puoi scaricare gli adattatori ottimizzati dalla tua istanza COS.

download_file_cos(credentials, "granite3_lora.zip", "granite3_lora.zip")
!unzip granite3_lora.zip

Ora configura le opzioni per ChatModel in modo che incorpori gli adattatori.

# setup inference configurations
args = dict(
  model_name_or_path="ibm-granite/granite-3.3-2b-instruct",  # use IBM Granite 3.3 2b instruct model
  adapter_name_or_path="granite3_lora", # load the saved LoRA adapters
  template="granite3", # set to the same one used in training, template for constructing prompts
  finetuning_type="lora", # which fine-tuning technique used in training
  infer_backend="huggingface" # choices: [huggingface, vllm]
)

# create inference config file to run with llama factory
with open("inference_config.yaml", "w", encoding="utf-8") as file:
  yaml.dump(args, file, indent=2)


from llamafactory.chat import ChatModel
chat_model = ChatModel(args)

Ora possiamo testare la stessa sfida di ragionamento al modello messo a punto:

messages = []

# run inference chatbot
question = '''
A 1-year-old girl is brought to a neurologist due to increasing seizure frequency over the past 2 months. 
She recently underwent a neurology evaluation which revealed hypsarrhythmia on electroencephalography (EEG) with a mix of slow waves, multifocal spikes, and asynchrony. 
Her parents have noticed the patient occasionally stiffens and spreads her arms at home. She was born at 38-weeks gestational age without complications. 
She has no other medical problems. Her medications consist of lamotrigine and valproic acid. Her temperature is 98.3\u00b0F (36.8\u00b0C), blood pressure is 90/75 mmHg, pulse is 94/min, and respirations are 22/min. 
Physical exam reveals innumerable hypopigmented macules on the skin and an irregularly shaped, thickened, and elevated plaque on the lower back. 
Which of the following is most strongly associated with this patient's condition?"
"A": "Cardiac rhabdomyoma", "B": "Glaucoma", "C": "Optic glioma", "D": "Polyostotic fibrous dysplasia"
'''

Creare un nuovo messaggio usando la domanda e passarlo al modello messo a punto:

messages.append({"role": "user", "content": question})

response = ""
for new_text in chat_model.stream_chat(messages):
    response += new_text

print(response)
messages.append({"role": "assistant", "content": response})
Cardiac rhabdomyoma

Esempio di output dal modello messo a punto:

Utente:

A 1-year-old girl is brought to a neurologist due to increasing seizure frequency over the past 2 months. 
She recently underwent a neurology evaluation which revealed hypsarrhythmia on electroencephalography (EEG) with a mix of slow waves, multifocal spikes, and asynchrony. 
Her parents have noticed the patient occasionally stiffens and spreads her arms at home. She was born at 38-weeks gestational age without complications. 
She has no other medical problems. Her medications consist of lamotrigine and valproic acid. Her temperature is 98.3\u00b0F (36.8\u00b0C), blood pressure is 90/75 mmHg, pulse is 94/min, and respirations are 22/min. 
Physical exam reveals innumerable hypopigmented macules on the skin and an irregularly shaped, thickened, and elevated plaque on the lower back. 
Which of the following is most strongly associated with this patient's condition?"
"A": "Cardiac rhabdomyoma", "B": "Glaucoma", "C": "Optic glioma", "D": "Polyostotic fibrous dysplasia"

Risposta:

Cardiac rhabdomyoma

Questa volta il modello ha generato la risposta corretta, grazie all'adattatore addestrato.

Un aspetto da notare: il modello non risponde più con il suo ragionamento. Questo risultato è dovuto al fatto che il set di dati utilizzato per LoRA ha solo la risposta corretta come output previsto del modello. La messa a punto della LoRA può essere utilizzata sia per fornire nuove informazioni che per istruire il modello su come rispondere.

Riepilogo

In questo tutorial LoRA ha perfezionato il modello IBM® Granite-3.3-2b-Instruct con nuove conoscenze mediche e un modello dettagliato su come rispondere. Hai visto la capacità di apprendimento di Granite 3.3 anche con un modello piccolo e campioni limitati dal set di dati.

Soluzioni correlate
Foundation Model

Esplora la libreria IBM di foundation model nel portfolio di watsonx per scalare in sicurezza l'AI generativa per la tua azienda.

Scopri watsonx.ai
Soluzioni di intelligenza artificiale

Metti l'AI al servizio della tua azienda grazie all'esperienza leader di settore e alla gamma di soluzioni di IBM nel campo dell'AI.

Esplora le soluzioni AI
Consulenza e servizi sull'AI

Reinventa i flussi di lavoro e le operazioni critiche aggiungendo l'AI per massimizzare le esperienze, il processo decisionale in tempo reale e il valore aziendale.

Esplora i servizi AI
Fai il passo successivo

Esplora la libreria IBM dei foundation model nel portfolio di IBM watsonx per scalare in sicurezza l'AI generativa per la tua azienda.

Scopri watsonx.ai Esplora i modelli AI Granite di IBM