Spaces:
Runtime error
Runtime error
root commited on
Commit ·
d8cec1e
0
Parent(s):
initial
Browse files- .gitignore +2 -0
- Dockerfile +21 -0
- Readme.md +13 -0
- app.py +54 -0
- compose.yaml +18 -0
- config.py +18 -0
- get_data.py +153 -0
- model.py +41 -0
- requirements.txt +11 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model/
|
| 2 |
+
tokenizer.model
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FROM python:3.8.17
|
| 2 |
+
FROM pytorch/pytorch:2.0.0-cuda11.7-cudnn8-devel
|
| 3 |
+
|
| 4 |
+
WORKDIR /code
|
| 5 |
+
|
| 6 |
+
# Get model artifacts from google cloud storage
|
| 7 |
+
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - && apt-get update -y && apt-get install google-cloud-cli -y
|
| 8 |
+
|
| 9 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 10 |
+
|
| 11 |
+
RUN apt update
|
| 12 |
+
|
| 13 |
+
# RUN apt-get install -y python3 python3-pip
|
| 14 |
+
|
| 15 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 16 |
+
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
CMD ["python3", "app.py"]
|
Readme.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Introduction
|
| 2 |
+
|
| 3 |
+
In the leadup to many US Supreme Court cases, there is often rampant speculation and forecasting by legal experts, the media, and the general public as to what the outcome will be. There is sometimes consensus on the expected rulings, which seem to be based on the existing rulings made by the court and its Justices. Predictions based on existing publicly accessible text? Sounds like a deep learning project! This project makes a rudimentary attempt at predicting the Supreme Court’s ruling on a case when provided a brief overview of the case.
|
| 4 |
+
|
| 5 |
+
## Training Data
|
| 6 |
+
|
| 7 |
+
Training data for this project was gathered from Justia using the basic requests library and consists of case summaries and court opinions dating back to 2013 and consists of over 700 cases. I collected both court opinions and the opinions of individual justices, but at the current time only the court opinions were used for fine tuning.
|
| 8 |
+
|
| 9 |
+
## Model
|
| 10 |
+
The base model is Meta’s Llama2 7B, chosen because it can be trained on an 8GB consumer GPU with quantization. The model finetuning was performed on a laptop RTX 4060 using 4bit normal float quantization and Low-Rank adapters through the Hugging Face transformers and PEFT libraries. LoRA updates were merged with the model following training completion.
|
| 11 |
+
|
| 12 |
+
## Deployment
|
| 13 |
+
This app runs as a gradio app inside a docker container deployed to Google Cloud Run on a T4 instance. The model weights themselves are stored on Google Cloud storage.
|
app.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import random
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
import time
|
| 5 |
+
import gradio as gr
|
| 6 |
+
|
| 7 |
+
from config import model_path, tokenizer_path
|
| 8 |
+
from model import Model
|
| 9 |
+
|
| 10 |
+
class CaseSummaries:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self._case_summaries = None
|
| 13 |
+
|
| 14 |
+
@property
|
| 15 |
+
def case_summaries(self):
|
| 16 |
+
if self._case_summaries == None:
|
| 17 |
+
r = requests.get('https://en.wikipedia.org/wiki/List_of_pending_United_States_Supreme_Court_cases')
|
| 18 |
+
soup = BeautifulSoup(r.content, 'html')
|
| 19 |
+
self._case_summaries = soup.find_all(True, {'class': ['wikitable', 'sortable', 'jquery-tablesorter']})[0].find_all('tr')[1:]
|
| 20 |
+
return self._case_summaries
|
| 21 |
+
|
| 22 |
+
def random_summary(self) -> str:
|
| 23 |
+
# function to get a random case summary from wikipedia pending list
|
| 24 |
+
row:str = random.choice(self.case_summaries)
|
| 25 |
+
summary = row.find_all('td')[2]
|
| 26 |
+
summary = summary.get_text()
|
| 27 |
+
return summary
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def run():
|
| 31 |
+
|
| 32 |
+
summaries = CaseSummaries()
|
| 33 |
+
case_placeholder = summaries.random_summary()
|
| 34 |
+
model = Model(model_path=model_path, tokenzier_path=tokenizer_path)
|
| 35 |
+
with gr.Blocks() as demo:
|
| 36 |
+
cache = gr.Textbox(visible=False)
|
| 37 |
+
with gr.Row():
|
| 38 |
+
btn = gr.Button(value="Get Random Case")
|
| 39 |
+
btn2 = gr.Button(value="Run")
|
| 40 |
+
|
| 41 |
+
with gr.Row():
|
| 42 |
+
txt = gr.Textbox(label="Case Description", lines=15, value=case_placeholder)
|
| 43 |
+
txt2 = gr.Chatbot(label='Predicted Court Opinion')
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
btn.click(summaries.random_summary, outputs=[txt], queue=False)
|
| 47 |
+
# btn2.click(inference, inputs=[txt], outputs=[txt2])
|
| 48 |
+
btn2.click(lambda x: x, txt, cache, queue=False).then(
|
| 49 |
+
model.inference, cache, txt2)
|
| 50 |
+
|
| 51 |
+
demo.queue().launch(server_name= "0.0.0.0", share=False)
|
| 52 |
+
|
| 53 |
+
if __name__=='__main__':
|
| 54 |
+
run()
|
compose.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
gradio:
|
| 3 |
+
environment:
|
| 4 |
+
- ENVIRONMENT=gcp
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
+
deploy:
|
| 9 |
+
resources:
|
| 10 |
+
reservations:
|
| 11 |
+
devices:
|
| 12 |
+
- driver: nvidia
|
| 13 |
+
count: 1
|
| 14 |
+
capabilities: [gpu]
|
| 15 |
+
command: python3 app.py
|
| 16 |
+
|
| 17 |
+
ports:
|
| 18 |
+
- "7860:7860"
|
config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'''Set ENVIRONMENT to local for local app.py testing and spaces for running from docker image'''
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
env = os.environ.get('ENVIRONMENT', 'local')
|
| 5 |
+
|
| 6 |
+
if env == 'local':
|
| 7 |
+
model_path = '../merged_llama2/'
|
| 8 |
+
tokenizer_path = '../Llama2/7B/tokenizer.model'
|
| 9 |
+
|
| 10 |
+
elif env == 'spaces':
|
| 11 |
+
model_path = 'akfung/llama_supreme'
|
| 12 |
+
tokenizer_path = 'akfung/llama_supreme'
|
| 13 |
+
|
| 14 |
+
elif env == 'gcp':
|
| 15 |
+
model_path = 'model/'
|
| 16 |
+
tokenizer_path = 'tokenizer.model'
|
| 17 |
+
|
| 18 |
+
max_new_tokens = os.environ.get('max_new_tokens', 100)
|
get_data.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from bs4 import BeautifulSoup
|
| 2 |
+
import re
|
| 3 |
+
import requests
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
|
| 7 |
+
'''Script for scraping and formatting Supreme Court case summaries and court opinions from Justia since 2013'''
|
| 8 |
+
|
| 9 |
+
def remove_tags(text: str) -> str:
|
| 10 |
+
'''helper method'''
|
| 11 |
+
text = re.sub(r'\t|\r', '', text)
|
| 12 |
+
text = re.sub(r'\n', ' ', text)
|
| 13 |
+
text.encode('ascii', 'ignore').decode()
|
| 14 |
+
return text
|
| 15 |
+
|
| 16 |
+
def clean_soup(url, verbose=False):
|
| 17 |
+
'''Get docket no, court opinion, and justice opinions for a given case url'''
|
| 18 |
+
|
| 19 |
+
r = requests.get(url)
|
| 20 |
+
soup = BeautifulSoup(r.content, 'html')
|
| 21 |
+
case_details = dict()
|
| 22 |
+
|
| 23 |
+
docket_no = soup.find_all(True, {'class':['flex-col', 'reset-width-below-tablet', 'item']})[0].find_all('span')[0].get_text()
|
| 24 |
+
|
| 25 |
+
# get summary. if none, return empty
|
| 26 |
+
case_summaries = soup.find_all('div', id='summary')
|
| 27 |
+
if len(case_summaries) == 0:
|
| 28 |
+
return docket_no, {'summary': 'Dismissed'}
|
| 29 |
+
case_summary = case_summaries[0].get_text()
|
| 30 |
+
case_details['summary'] = remove_tags(case_summary)
|
| 31 |
+
|
| 32 |
+
# look for per curiam opinion
|
| 33 |
+
per_curiam = soup.find_all(True, {'data-gtm-label':['Opinions Tab - Per Curiam']})
|
| 34 |
+
if len(per_curiam)>0:
|
| 35 |
+
opinion = soup.find_all('div', {'id':re.compile(r'tab-opinion-\d+')})[0]
|
| 36 |
+
opinion = remove_tags(opinion.get_text())
|
| 37 |
+
court_opinion_text = re.findall(r'(?<=Per Curiam.)[\w\W]+', opinion)[0].strip()
|
| 38 |
+
case_details['court_opinion'] = court_opinion_text
|
| 39 |
+
else:
|
| 40 |
+
|
| 41 |
+
for opinion in soup.find_all('div', {'id': re.compile(r'tab-opinion-')})[1:]:
|
| 42 |
+
|
| 43 |
+
opinion = remove_tags(opinion.get_text())
|
| 44 |
+
|
| 45 |
+
justice_name = re.findall(r'(?<=Justice\s)\w+', opinion)[0]
|
| 46 |
+
if verbose:
|
| 47 |
+
print(justice_name)
|
| 48 |
+
|
| 49 |
+
# get court opinion text
|
| 50 |
+
court_opinion_text = re.findall(r'(?<=delivered the opinion of the Court.)[\w\W]+', opinion)
|
| 51 |
+
|
| 52 |
+
if len(court_opinion_text) >0:
|
| 53 |
+
justice_opinion = court_opinion_text[0].strip()
|
| 54 |
+
case_details['court_opinion'] = justice_opinion
|
| 55 |
+
case_details[justice_name] = justice_opinion
|
| 56 |
+
if verbose:
|
| 57 |
+
print(justice_opinion)
|
| 58 |
+
|
| 59 |
+
else:
|
| 60 |
+
justice_opinion = re.findall(r'((?<=dissenting.)[\w\W]+|(?<=concurring.)[\w\W]+)', opinion)[0].strip()
|
| 61 |
+
if verbose:
|
| 62 |
+
print(justice_opinion)
|
| 63 |
+
case_details[justice_name] = justice_opinion
|
| 64 |
+
return docket_no, case_details
|
| 65 |
+
|
| 66 |
+
if __name__=="__main__":
|
| 67 |
+
# Scrape case_urls from justia
|
| 68 |
+
|
| 69 |
+
years = [f'https://supreme.justia.com/cases/federal/us/year/{i}.html' for i in range(2013, 2024)]
|
| 70 |
+
case_urls = []
|
| 71 |
+
|
| 72 |
+
for year_url in years:
|
| 73 |
+
r = requests.get(year_url)
|
| 74 |
+
soup = BeautifulSoup(r.content, 'html')
|
| 75 |
+
for i in soup.find_all(True, {'class':['color-green', 'text-soft-wrap']} ):
|
| 76 |
+
case_urls.append('https://supreme.justia.com' + i.a['href'])
|
| 77 |
+
|
| 78 |
+
# scrape cases, track errors
|
| 79 |
+
case_data = dict()
|
| 80 |
+
failed_urls = []
|
| 81 |
+
i = 1
|
| 82 |
+
for url in case_urls:
|
| 83 |
+
print(f'Scraping case {i}')
|
| 84 |
+
try:
|
| 85 |
+
docket_no, opinions = clean_soup(url)
|
| 86 |
+
assert opinions
|
| 87 |
+
case_data[docket_no] = opinions
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f'Failed on url {url}')
|
| 90 |
+
failed_urls.append([url, str(e)])
|
| 91 |
+
|
| 92 |
+
i+=1
|
| 93 |
+
|
| 94 |
+
# Serialize case_data as json
|
| 95 |
+
json_object = json.dumps(case_data, indent=4)
|
| 96 |
+
|
| 97 |
+
# Writing to case_data.json
|
| 98 |
+
with open("case_data.json", "w") as outfile:
|
| 99 |
+
outfile.write(json_object)
|
| 100 |
+
|
| 101 |
+
# Create chunked dataset for hugging face dataloaders
|
| 102 |
+
|
| 103 |
+
data = []
|
| 104 |
+
current = 1
|
| 105 |
+
max_char = 4000
|
| 106 |
+
for k,v in case_data.items():
|
| 107 |
+
print(current)
|
| 108 |
+
current +=1
|
| 109 |
+
if v['summary'] != 'Dismissed':
|
| 110 |
+
summary = re.findall(r'(?<=Justia Summary\s\s\s)[\w\W]+', v['summary'])[0]
|
| 111 |
+
else:
|
| 112 |
+
continue
|
| 113 |
+
|
| 114 |
+
if v.get('court_opinion')==None:
|
| 115 |
+
|
| 116 |
+
continue
|
| 117 |
+
|
| 118 |
+
# remove notes from opinion
|
| 119 |
+
if re.search(r'[\w\W]+(?=Notes 1 \xa0)', v['court_opinion']):
|
| 120 |
+
court_opinion = re.findall(r'[\w\W]+(?=Notes 1 \xa0)', v['court_opinion'])[0]
|
| 121 |
+
|
| 122 |
+
if len(court_opinion) + len(summary) > max_char:
|
| 123 |
+
|
| 124 |
+
# chunk the opinions
|
| 125 |
+
max_len = max_char - len(summary)
|
| 126 |
+
|
| 127 |
+
chunk_size = int(max_len/ (math.ceil(max_len/ len(court_opinion))))
|
| 128 |
+
chunk_suffix = 1
|
| 129 |
+
for i in range(0, len(court_opinion), chunk_size):
|
| 130 |
+
chunk = court_opinion[i:i+chunk_size]
|
| 131 |
+
data.append({
|
| 132 |
+
'docket_no': k + str(chunk_suffix),
|
| 133 |
+
'summary': summary,
|
| 134 |
+
'opinion': chunk
|
| 135 |
+
})
|
| 136 |
+
|
| 137 |
+
chunk_suffix +=1
|
| 138 |
+
|
| 139 |
+
else:
|
| 140 |
+
data.append({
|
| 141 |
+
'docket_no': k,
|
| 142 |
+
'summary': summary,
|
| 143 |
+
'opinion': v.get('court_opinion', '')
|
| 144 |
+
})
|
| 145 |
+
|
| 146 |
+
dataloader_formatted = dict(
|
| 147 |
+
version='1.0',
|
| 148 |
+
data=data
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
json_object = json.dumps(dataloader_formatted, indent=4)
|
| 152 |
+
with open("chunked_case_data.json", "w") as outfile:
|
| 153 |
+
outfile.write(json_object)
|
model.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from transformers import LlamaForCausalLM, LlamaTokenizer, BitsAndBytesConfig, TextIteratorStreamer, pipeline
|
| 3 |
+
from threading import Thread
|
| 4 |
+
from config import model_path, tokenizer_path, max_new_tokens
|
| 5 |
+
|
| 6 |
+
class Model:
|
| 7 |
+
'''Client class for holding Llama2 model and tokenizer. Models are loaded according to
|
| 8 |
+
ENVIRONMENT environment variable
|
| 9 |
+
'''
|
| 10 |
+
def __init__(self,
|
| 11 |
+
model_path:str=model_path,
|
| 12 |
+
tokenzier_path:str=tokenizer_path,
|
| 13 |
+
max_new_tokens:int=max_new_tokens):
|
| 14 |
+
self.model_path = model_path
|
| 15 |
+
self.tokenzier_path = tokenzier_path
|
| 16 |
+
self.max_new_tokens = max_new_tokens
|
| 17 |
+
self.bnb_config = BitsAndBytesConfig(
|
| 18 |
+
load_in_4bit=True,
|
| 19 |
+
bnb_4bit_use_double_quant=True,
|
| 20 |
+
bnb_4bit_quant_type="nf4",
|
| 21 |
+
bnb_4bit_compute_dtype=torch.float16,
|
| 22 |
+
)
|
| 23 |
+
self.model = LlamaForCausalLM.from_pretrained(model_path, quantization_config=self.bnb_config)
|
| 24 |
+
self.tokenizer = LlamaTokenizer.from_pretrained(tokenzier_path)
|
| 25 |
+
self.streamer = TextIteratorStreamer(self.tokenizer, skip_prompt=True)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def inference(self, query: str):
|
| 29 |
+
'''Inference function for gradio text streaming'''
|
| 30 |
+
query = "You are the US Supreme Court. What is your opinion on the following case? " + query
|
| 31 |
+
batch = self.tokenizer(query, return_tensors='pt')
|
| 32 |
+
|
| 33 |
+
generation_kwargs = dict(batch, streamer=self.streamer, max_new_tokens=self.max_new_tokens, repetition_penalty=1.1)
|
| 34 |
+
thread = Thread(target=self.model.generate, kwargs=generation_kwargs)
|
| 35 |
+
thread.start()
|
| 36 |
+
|
| 37 |
+
output= [[None, '']]
|
| 38 |
+
|
| 39 |
+
for word in self.streamer:
|
| 40 |
+
output[0][1] += word
|
| 41 |
+
yield output
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
requests==2.31.0
|
| 2 |
+
gradio==3.39.0
|
| 3 |
+
beautifulsoup4==4.12.2
|
| 4 |
+
transformers==4.31.0
|
| 5 |
+
torch==2.0.0
|
| 6 |
+
sentencepiece==0.1.99
|
| 7 |
+
regex==2023.6.3
|
| 8 |
+
bitsandbytes==0.39.1
|
| 9 |
+
accelerate==0.20.3
|
| 10 |
+
peft==0.4.0
|
| 11 |
+
scipy==1.10.1
|