root commited on
Commit
d8cec1e
·
0 Parent(s):
Files changed (9) hide show
  1. .gitignore +2 -0
  2. Dockerfile +21 -0
  3. Readme.md +13 -0
  4. app.py +54 -0
  5. compose.yaml +18 -0
  6. config.py +18 -0
  7. get_data.py +153 -0
  8. model.py +41 -0
  9. 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