NLP: zero findings, and two real bugs found on the way

384 findings to none across ten scripts. Most of it is annotation and
docstring work, but the lint surfaced two defects that had nothing to do with
style:

- `gpt_chunks.py` stripped its GPT preamble with
  `input_string.strip("iSTS chunks:")`. str.strip takes a SET OF CHARACTERS,
  not a prefix, so it also ate any leading i/S/T/s/c/h/u/n/k/space/colon:
  "iSTS chunks: sentence one" came out as "entence one", silently losing the
  first letter of every chunked sentence. Now removeprefix. ruff found it as
  B005 + PLE1310.
- `is_numbered_line_format` had `for string in string` in its list branch,
  which raises NameError the moment a list is passed (F821).
- `processing.test_XML()` read a module-level `alignments_data` that does not
  exist at module scope; it only ever ran when pasted into a live session. It
  takes the data as an argument now and is renamed `log_first_alignment` so
  pytest stops collecting a non-test as a test.

Also real rather than cosmetic:

- The OpenAI key was a placeholder string literal in three files, one of them
  carrying a "Replace me with os.environ['API_KEY']" comment. All three read
  OPENAI_API_KEY now and say so when it is missing -- as committed, none of
  these scripts could actually run.
- `rating.rate_similarity` was submitted unfinished: it built a client, never
  touched its two arguments and returned None despite `-> str`. It now makes
  the same call the rest of the package makes.
- The two 2 kB alignment prompts and one 344-character chunking prompt moved
  to prompts/*.txt. Wrapping them in source would have put newlines into the
  middle of the prompt.
- `format_alignments` parsed GPT's replies with `eval()`; the file holds one
  repr()'d string per line, so ast.literal_eval is both sufficient and safe.
- NLP/__init__.py deleted -- it made the course directory a package named
  "NLP", which is an invalid module name (N999) and which nothing imports;
  the scripts use plain `import processing`.

INP001 is settled the same way throughout: a coursework file run as
`python x.py` gets a shebang and the executable bit, which is what it is,
rather than an __init__.py that would make a course code into a module name.

Verified: every module compiles and `./run.sh run Programming/NLP` is clean.
The scripts themselves need the SemEval corpus and a paid API key, so they
are not executed here.

Repo-wide: 3,159 findings -> 2,775.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
This commit is contained in:
Krzysztof Rudnicki 2026-08-28 20:13:19 +02:00
parent a8a2710ebd
commit b017c7500e
14 changed files with 595 additions and 371 deletions

43
Programming/NLP/compare_chunks.py Normal file → Executable file
View File

@ -1,10 +1,23 @@
def reformat_sentence(sentence): #!/usr/bin/env python3
"""Compare kuhy's GPT chunkings against the SemEval gold standard.
Reads one GPT output file and the corresponding gold chunk file, normalises
the GPT side, and reports every sentence where the two disagree on words.
"""
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
def reformat_sentence(sentence: str) -> str:
"""Reformats a sentence by replacing slashes '/' with ' ]'.""" """Reformats a sentence by replacing slashes '/' with ' ]'."""
return sentence.replace("/", " ]") return sentence.replace("/", " ]")
def insert_commas(sentence, reference_sentence): def insert_commas(sentence: str, reference_sentence: str) -> str:
"""Inserts commas into 'sentence' at the same positions as they appear in 'reference_sentence'.""" """Insert commas into *sentence* where *reference_sentence* has them."""
# Splitting the sentences into chunks # Splitting the sentences into chunks
sentence_chunks = sentence.split(" ] [ ") sentence_chunks = sentence.split(" ] [ ")
reference_chunks = reference_sentence.split(" ] [ ") reference_chunks = reference_sentence.split(" ] [ ")
@ -18,11 +31,13 @@ def insert_commas(sentence, reference_sentence):
return " ] [ ".join(sentence_chunks) return " ] [ ".join(sentence_chunks)
def process_sentences(sentences1, sentences2): def process_sentences(
"""Processes two lists of sentences according to the specified rules.""" sentences1: list[str], sentences2: list[str]
) -> list[dict[str, object]]:
"""Compare each GPT chunking against its gold counterpart."""
processed_sentences = [] processed_sentences = []
for sentence1, sentence2 in zip(sentences1, sentences2): for sentence1, sentence2 in zip(sentences1, sentences2, strict=False):
# Reformat the first sentence # Reformat the first sentence
reformatted_sentence1 = reformat_sentence(sentence1) reformatted_sentence1 = reformat_sentence(sentence1)
reformatted_sentence1 = insert_commas(reformatted_sentence1, sentence2) reformatted_sentence1 = insert_commas(reformatted_sentence1, sentence2)
@ -50,7 +65,7 @@ def process_sentences(sentences1, sentences2):
if "[ . ]" in sentence2: if "[ . ]" in sentence2:
output_sentence += " [ . ]" output_sentence += " [ . ]"
# Adding the differences and the output sentence to the processed_sentences list # Record the differences alongside the merged sentence.
processed_sentences.append( processed_sentences.append(
{ {
"sentence1": reformatted_sentence1.strip(), "sentence1": reformatted_sentence1.strip(),
@ -60,21 +75,21 @@ def process_sentences(sentences1, sentences2):
} }
) )
if diff and diff != {"."}: if diff and diff != {"."}:
print("Difference found!") logger.info("Difference found!")
print("Sentence 1:", reformatted_sentence1.strip()) logger.info("Sentence 1: %s", reformatted_sentence1.strip())
print("Sentence 2:", sentence2.strip()) logger.info("Sentence 2: %s", sentence2.strip())
print("Differences:", diff) logger.info("Differences: %s", diff)
print("\n") logger.info("\n")
return processed_sentences return processed_sentences
# First, let's open and read the contents of both files to understand their structure and content. # The GPT output and the gold chunking for the same corpus half.
file_path_1 = "test_goldStandard/images/images-chunks-gpt-one.txt" file_path_1 = "test_goldStandard/images/images-chunks-gpt-one.txt"
file_path_2 = "test_goldStandard/images/STSint.testinput.images.sent1.chunk.txt" file_path_2 = "test_goldStandard/images/STSint.testinput.images.sent1.chunk.txt"
# Reading the files # Reading the files
with open(file_path_1) as file1, open(file_path_2) as file2: with Path(file_path_1).open() as file1, Path(file_path_2).open() as file2:
sentences_file_1 = file1.readlines() sentences_file_1 = file1.readlines()
sentences_file_2 = file2.readlines() sentences_file_2 = file2.readlines()

20
Programming/NLP/create_alignments.py Normal file → Executable file
View File

@ -1,3 +1,8 @@
#!/usr/bin/env python3
"""Ask GPT to align the headline chunks, and save the raw replies."""
from pathlib import Path
import gpt_alignment import gpt_alignment
import pandas as pd import pandas as pd
import processing import processing
@ -6,9 +11,6 @@ import processing
file_path = "alignments_unformatted_headlines.txt" file_path = "alignments_unformatted_headlines.txt"
# paths to students answers database # paths to students answers database
# chunked_path1 = "test_goldStandard/student/STSint.testinput.answers-students.sent1.chunk.txt"
# chunked_path2 = "test_goldStandard/student/STSint.testinput.answers-students.sent2.chunk.txt"
# alignment_path = "test_goldStandard/student/STSint.testinput.answers-students.wa"
# paths to headlines # paths to headlines
chunked_path1 = "test_goldStandard/headlines/STSint.testinput.headlines.sent1.chunk.txt" chunked_path1 = "test_goldStandard/headlines/STSint.testinput.headlines.sent1.chunk.txt"
@ -16,23 +18,19 @@ chunked_path2 = "test_goldStandard/headlines/STSint.testinput.headlines.sent1.ch
alignment_path = "test_goldStandard/headlines/STSint.testinput.headlines.wa" alignment_path = "test_goldStandard/headlines/STSint.testinput.headlines.wa"
# load data # load data
# studentAnserws = processing.load_sentences(studentAnswers1_path, studentAnswers1_path)
goldstandard_chunked = processing.load_chunked(chunked_path1, chunked_path2) goldstandard_chunked = processing.load_chunked(chunked_path1, chunked_path2)
goldstandard_alignment = processing.load_alignment(alignment_path) goldstandard_alignment = processing.load_alignment(alignment_path)
# get a nice anwser-student table # get a nice anwser-student table
data = pd.merge( data = pd.DataFrame.merge(
goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True
) )
# print(data)
data_for_chat, indexes = processing.get_chunks_as_text(data) data_for_chat, indexes = processing.get_chunks_as_text(data)
client = gpt_alignment.createGPT() client = gpt_alignment.create_gpt()
responses = [] responses = [gpt_alignment.call_api(client, chunks) for chunks in data_for_chat]
for i in range(len(data_for_chat)):
responses.append(gpt_alignment.callApi(client, data_for_chat[i]))
# Writing to the file with repr() to preserve "\n" characters # Writing to the file with repr() to preserve "\n" characters
with open(file_path, "w") as file: with Path(file_path).open("w") as file:
file.writelines(repr(string) + "\n" for string in responses) file.writelines(repr(string) + "\n" for string in responses)

36
Programming/NLP/create_alignments_with_examples.py Normal file → Executable file
View File

@ -1,14 +1,19 @@
#!/usr/bin/env python3
"""Ask GPT to align chunks, few-shot prompted with gold examples."""
import logging
from pathlib import Path
import gpt_alignment import gpt_alignment
import pandas as pd import pandas as pd
import processing import processing
logger = logging.getLogger(__name__)
# Specify the output file path # Specify the output file path
file_path = "alignments_with_training_headlines2.wa" file_path = "alignments_with_training_headlines2.wa"
# paths to students andsewrs database # paths to students andsewrs database
# chunked_path1 = "test_goldStandard/student/STSint.testinput.answers-students.sent1.chunk.txt"
# chunked_path2 = "test_goldStandard/student/STSint.testinput.answers-students.sent2.chunk.txt"
# alignment_path = "test_goldStandard/student/STSint.testinput.answers-students.wa"
# paths to headlines # paths to headlines
chunked_path1 = "test_goldStandard/headlines/STSint.testinput.headlines.sent1.chunk.txt" chunked_path1 = "test_goldStandard/headlines/STSint.testinput.headlines.sent1.chunk.txt"
@ -20,10 +25,9 @@ goldstandard_chunked = processing.load_chunked(chunked_path1, chunked_path2)
goldstandard_alignment = processing.load_alignment(alignment_path) goldstandard_alignment = processing.load_alignment(alignment_path)
# get a nice anwser-student table # get a nice anwser-student table
data = pd.merge( data = pd.DataFrame.merge(
goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True
) )
# print(data)
train, test = processing.generate_train_test_split(data) train, test = processing.generate_train_test_split(data)
@ -31,18 +35,20 @@ data_for_chat, indexes = processing.get_chunks_as_text(test)
_, indexes_of_training = processing.get_chunks_as_text(train) _, indexes_of_training = processing.get_chunks_as_text(train)
indexes_of_training = [i + 1 for i in indexes_of_training] indexes_of_training = [i + 1 for i in indexes_of_training]
indexes = [i + 1 for i in indexes] indexes = [i + 1 for i in indexes]
print(indexes_of_training) logger.info("%s", indexes_of_training)
print(indexes) logger.info("%s", indexes)
client = gpt_alignment.createGPT() client = gpt_alignment.create_gpt()
responses = [] # Ten only: each call is a paid few-shot request, and ten was enough for the
for i in range(10): # len(data_for_chat)): # report's comparison.
responses.append( SAMPLE_SIZE = 10
[gpt_alignment.callApi_examples(client, train, data_for_chat[i]), indexes[i]] responses = [
) [gpt_alignment.call_api_examples(client, train, data_for_chat[i]), indexes[i]]
for i in range(SAMPLE_SIZE)
]
with open(file_path, "w") as file: with Path(file_path).open("w") as file:
for i, r in enumerate(responses): for r in responses:
file.write('<sentence id="' + str(r[1] + 1) + '" status="">\n') file.write('<sentence id="' + str(r[1] + 1) + '" status="">\n')
file.write("<alignment>\n") file.write("<alignment>\n")
file.write(r[0]) file.write(r[0])

66
Programming/NLP/format_alignments.py Normal file → Executable file
View File

@ -1,9 +1,17 @@
#!/usr/bin/env python3
"""Turn the raw GPT alignment replies into the SemEval .wa format."""
import ast
import copy import copy
import logging
import re import re
from pathlib import Path
import pandas as pd import pandas as pd
import processing import processing
logger = logging.getLogger(__name__)
# input file name # input file name
file_path = "alignments_unformatted_headlines.txt" file_path = "alignments_unformatted_headlines.txt"
@ -16,26 +24,32 @@ chunked_path2 = "test_goldStandard/headlines/STSint.testinput.headlines.sent1.ch
alignment_path = "test_goldStandard/headlines/STSint.testinput.headlines.wa" alignment_path = "test_goldStandard/headlines/STSint.testinput.headlines.wa"
# paths to students andsewrs database # paths to students andsewrs database
# chunked_path1 = "test_goldStandard/student/STSint.testinput.answers-students.sent1.chunk.txt"
# chunked_path2 = "test_goldStandard/student/STSint.testinput.answers-students.sent2.chunk.txt"
# alignment_path = "test_goldStandard/student/STSint.testinput.answers-students.wa"
# load data # load data
goldstandard_chunked = processing.load_chunked(chunked_path1, chunked_path2) goldstandard_chunked = processing.load_chunked(chunked_path1, chunked_path2)
goldstandard_alignment = processing.load_alignment(alignment_path) goldstandard_alignment = processing.load_alignment(alignment_path)
# ASCII letters mark a chunk the numbering pass failed to replace, and a
# formatted alignment always has at least the four //-separated fields.
ASCII_MAX = 128
FULL_FIELDS = 4
SHORT_FIELDS = 3
# get a nice table # get a nice table
data = pd.merge( data = pd.DataFrame.merge(
goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True
) )
# open generated alignments # open generated alignments
with open(file_path) as file: # The file holds one repr()'d string per line (see create_alignments.py),
responses = [eval(line.strip()) for line in file] # so literal_eval is enough -- eval() here would execute whatever GPT
# happened to return.
with Path(file_path).open() as file:
responses = [ast.literal_eval(line.strip()) for line in file]
for i, r in enumerate(responses): for i, r in enumerate(responses):
print("\nresponse number " + str(i)) logger.info("%s", "\nresponse number " + str(i))
print(r) logger.info("%s", r)
unformatted = copy.deepcopy(responses) unformatted = copy.deepcopy(responses)
@ -82,41 +96,41 @@ for i, response in enumerate(responses):
chunk1arr = data.iloc[i]["chunked_sentance1"] chunk1arr = data.iloc[i]["chunked_sentance1"]
q = 1 q = 1
numberList = [] number_list = []
for chunk in chunk1arr: for raw_chunk in chunk1arr:
chunk = re.sub(r"(?<!\n) +(?!\n)", " ", chunk) chunk = re.sub(r"(?<!\n) +(?!\n)", " ", raw_chunk)
n_of_words = len(chunk.strip().split(" ")) n_of_words = len(chunk.strip().split(" "))
index_str = "" index_str = ""
for qq in range(q, q + n_of_words): for qq in range(q, q + n_of_words):
index_str = index_str + str(qq) + " " index_str = index_str + str(qq) + " "
numberList.append(index_str) number_list.append(index_str)
q = q + n_of_words q = q + n_of_words
for j, chunk in enumerate(data.iloc[i]["chunked_sentance1"]): for j, chunk in enumerate(data.iloc[i]["chunked_sentance1"]):
pattern = re.compile(chunk, re.IGNORECASE) pattern = re.compile(chunk, re.IGNORECASE)
temp[k][0] = pattern.sub(numberList[j], temp[k][0]) temp[k][0] = pattern.sub(number_list[j], temp[k][0])
chunk1arr = data.iloc[i]["chunked_sentance2"] chunk1arr = data.iloc[i]["chunked_sentance2"]
q = 1 q = 1
numberList = [] number_list = []
for chunk in chunk1arr: for raw_chunk in chunk1arr:
chunk = re.sub(r"(?<!\n) +(?!\n)", " ", chunk) chunk = re.sub(r"(?<!\n) +(?!\n)", " ", raw_chunk)
n_of_words = len(chunk.strip().split(" ")) n_of_words = len(chunk.strip().split(" "))
index_str = "" index_str = ""
for qq in range(q, q + n_of_words): for qq in range(q, q + n_of_words):
index_str = index_str + str(qq) + " " index_str = index_str + str(qq) + " "
numberList.append(index_str) number_list.append(index_str)
q = q + n_of_words q = q + n_of_words
for j, chunk in enumerate(data.iloc[i]["chunked_sentance2"]): for j, chunk in enumerate(data.iloc[i]["chunked_sentance2"]):
pattern = re.compile(chunk, re.IGNORECASE) pattern = re.compile(chunk, re.IGNORECASE)
temp[k][1] = pattern.sub(numberList[j], temp[k][1]) temp[k][1] = pattern.sub(number_list[j], temp[k][1])
if any(char.isalpha() and ord(char) < 128 for char in temp[k][0]) or any( if any(char.isalpha() and ord(char) < ASCII_MAX for char in temp[k][0]) or any(
char.isalpha() and ord(char) < 128 for char in temp[k][1] char.isalpha() and ord(char) < ASCII_MAX for char in temp[k][1]
): ):
temp[k] = "" temp[k] = ""
if len(temp[k]) >= 4: if len(temp[k]) >= FULL_FIELDS:
temp[k][3] = temp[k][3].replace("NOALI", "0") temp[k][3] = temp[k][3].replace("NOALI", "0")
if temp[k][3] == "": if temp[k][3] == "":
temp[k][3] = " 0 " temp[k][3] = " 0 "
@ -131,7 +145,7 @@ for i, response in enumerate(responses):
+ " // " + " // "
+ temp[k][3] + temp[k][3]
) )
elif len(temp[k]) == 3: elif len(temp[k]) == SHORT_FIELDS:
temp[k] = temp[k][0] + " <==> " + temp[k][1] + " // " + temp[k][2] + " // 0" temp[k] = temp[k][0] + " <==> " + temp[k][1] + " // " + temp[k][2] + " // 0"
temp[k] = re.sub(r"\s{2,}", " ", temp[k]) # remove double space temp[k] = re.sub(r"\s{2,}", " ", temp[k]) # remove double space
@ -152,11 +166,11 @@ for n, r in enumerate(responses):
indexes.append(n + 1) indexes.append(n + 1)
responses_final.append(r) responses_final.append(r)
print("rejected indexes:") logger.info("rejected indexes:")
print(rejected_indexes) logger.info("%s", rejected_indexes)
with open(output_file_path, "w") as file: with Path(output_file_path).open("w") as file:
for i, r in zip(indexes, responses_final): for i, r in zip(indexes, responses_final, strict=False):
file.write('<sentence id="' + str(i + 1) + '" status="">\n') file.write('<sentence id="' + str(i + 1) + '" status="">\n')
file.write("<alignment>\n") file.write("<alignment>\n")
file.write(r) file.write(r)

67
Programming/NLP/format_alignments_with_training.py Normal file → Executable file
View File

@ -1,35 +1,47 @@
#!/usr/bin/env python3
"""Format the few-shot alignment replies, and score them against gold."""
import copy import copy
import logging
import re import re
from pathlib import Path
import pandas as pd import pandas as pd
import processing import processing
logger = logging.getLogger(__name__)
# paths to students andsewrs database # paths to students andsewrs database
studentAnswers1_path = ( student_answers1_path = (
"test_goldStandard/student/STSint.testinput.answers-students.sent1.txt" "test_goldStandard/student/STSint.testinput.answers-students.sent1.txt"
) )
studentAnswers2_path = ( student_answers2_path = (
"test_goldStandard/student/STSint.testinput.answers-students.sent2.txt" "test_goldStandard/student/STSint.testinput.answers-students.sent2.txt"
) )
studentAnsewrs_chunked_path1 = ( student_answers_chunked_path1 = (
"test_goldStandard/student/STSint.testinput.answers-students.sent1.chunk.txt" "test_goldStandard/student/STSint.testinput.answers-students.sent1.chunk.txt"
) )
studentAnsewrs_chunked_path2 = ( student_answers_chunked_path2 = (
"test_goldStandard/student/STSint.testinput.answers-students.sent2.chunk.txt" "test_goldStandard/student/STSint.testinput.answers-students.sent2.chunk.txt"
) )
studentsAnsewrs_alignment_path = ( student_answers_alignment_path = (
"test_goldStandard/student/STSint.testinput.answers-students.wa" "test_goldStandard/student/STSint.testinput.answers-students.wa"
) )
# load data # load data
studentAnserws = processing.load_sentences(studentAnswers1_path, studentAnswers1_path) student_answers = processing.load_sentences(
goldstandard_chunked = processing.load_chunked( student_answers1_path, student_answers1_path
studentAnsewrs_chunked_path1, studentAnsewrs_chunked_path2
) )
goldstandard_alignment = processing.load_alignment(studentsAnsewrs_alignment_path) goldstandard_chunked = processing.load_chunked(
student_answers_chunked_path1, student_answers_chunked_path2
)
goldstandard_alignment = processing.load_alignment(student_answers_alignment_path)
SHORT_FIELDS = 3
MIN_FIELDS = 2
# get a nice anwser-student table # get a nice anwser-student table
data = pd.merge( data = pd.DataFrame.merge(
goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True goldstandard_chunked, goldstandard_alignment, left_index=True, right_index=True
) )
@ -41,8 +53,8 @@ responses = responses["alignment_text"].to_list()
for i, r in enumerate(responses): for i, r in enumerate(responses):
print("\nresponse number " + str(i)) logger.info("%s", "\nresponse number " + str(i))
print(r) logger.info("%s", r)
unformatted = copy.deepcopy(responses) unformatted = copy.deepcopy(responses)
@ -71,28 +83,21 @@ for i, response in enumerate(responses):
temp = re.sub(r"(?<!\n) +(?!\n)", " ", temp) # remove double space temp = re.sub(r"(?<!\n) +(?!\n)", " ", temp) # remove double space
temp = temp.split("\n") temp = temp.split("\n")
for k, t in enumerate(temp): for k in range(len(temp)):
temp[k] = temp[k].split("<==>") temp[k] = temp[k].split("<==>")
temp[k][1] = temp[k][1].split("//") temp[k][1] = temp[k][1].split("//")
# for j, chunk in enumerate(data.iloc[i]["chunked_sentance1"].sorted(key=len, reverse=True)):
for j, chunk in enumerate( for j, chunk in enumerate(
sorted( sorted(data.iloc[i]["chunked_sentance1"], key=len, reverse=True)
data.iloc[i]["chunked_sentance1"], key=lambda x: len(x), reverse=True
)
): ):
pattern = re.compile(chunk, re.IGNORECASE) pattern = re.compile(chunk, re.IGNORECASE)
temp[k][0] = pattern.sub(str(j + 1), temp[k][0]) temp[k][0] = pattern.sub(str(j + 1), temp[k][0])
# temp[k][0] = temp[k][0].replace(chunk, str(j+1))
for j, chunk in enumerate( for j, chunk in enumerate(
sorted( sorted(data.iloc[i]["chunked_sentance2"], key=len, reverse=True)
data.iloc[i]["chunked_sentance2"], key=lambda x: len(x), reverse=True
)
): ):
pattern = re.compile(chunk, re.IGNORECASE) pattern = re.compile(chunk, re.IGNORECASE)
temp[k][1][0] = pattern.sub(str(j + 1), temp[k][1][0]) temp[k][1][0] = pattern.sub(str(j + 1), temp[k][1][0])
# temp[k][1][0] = temp[k][1][0].replace(chunk, str(j+1)) if len(temp[k][1]) >= SHORT_FIELDS:
if len(temp[k][1]) >= 3:
temp[k] = ( temp[k] = (
temp[k][0] temp[k][0]
+ " <==> " + " <==> "
@ -102,7 +107,7 @@ for i, response in enumerate(responses):
+ " // " + " // "
+ temp[k][1][2] + temp[k][1][2]
) )
elif len(temp[k][1]) == 2: elif len(temp[k][1]) == MIN_FIELDS:
temp[k] = ( temp[k] = (
temp[k][0] + " <==> " + temp[k][1][0] + " // " + temp[k][1][1] + " // 0" temp[k][0] + " <==> " + temp[k][1][0] + " // " + temp[k][1][1] + " // 0"
) )
@ -111,19 +116,19 @@ for i, response in enumerate(responses):
responses[i] = "\n".join(temp) responses[i] = "\n".join(temp)
print("\nafter formatting\n") logger.info("\nafter formatting\n")
for i, r in enumerate(responses): for i, r in enumerate(responses):
print("\nresponse number " + str(i)) logger.info("%s", "\nresponse number " + str(i))
print("FORMATTED\n") logger.info("FORMATTED\n")
print(r) logger.info("%s", r)
print("\nUNFORMATTED\n") logger.info("\nUNFORMATTED\n")
print(unformatted[i]) logger.info("%s", unformatted[i])
# write to file # write to file
file_path = "student_fixed_format_with_training.txt" file_path = "student_fixed_format_with_training.txt"
with open(file_path, "w") as file: with Path(file_path).open("w") as file:
for i, r in enumerate(responses): for i, r in enumerate(responses):
file.write('<sentence id="' + str(i + 1) + '" status="">\n') file.write('<sentence id="' + str(i + 1) + '" status="">\n')
file.write("<alignment>\n") file.write("<alignment>\n")

87
Programming/NLP/gpt_alignment.py Normal file → Executable file
View File

@ -1,73 +1,54 @@
"""Get chunk alignment from chatGPT.""" #!/usr/bin/env python3
"""Get chunk alignment from ChatGPT.
The two prompts live in ``prompts/`` rather than in this file: they are 2 kB
of instructions each, on lines far past any line-length limit, and wrapping
them here would put newlines into the middle of the prompt itself.
"""
import os
from pathlib import Path
import pandas as pd import pandas as pd
from openai import OpenAI from openai import OpenAI
thePrompt = """You are a machine designed to align chunks from 2 sentences. This means you will be taking each chunk from one sentence and comparing it to every chunk from the other sentence. Choose the chunks with the strongest relation and assign them a score. PROMPTS = Path(__file__).parent / "prompts"
THE_PROMPT = (PROMPTS / "alignment.txt").read_text(encoding="utf-8")
The possible relations are: THE_PROMPT_WITH_COMMENT = (PROMPTS / "alignment_with_comment.txt").read_text(
EQUI: both chunks have the same meaning, they are semantically equivalent in this context. encoding="utf-8"
OPPO: the meanings of the chunks are in opposition to each other, lying in an inherently incompatible binary relationship. )
SPE1: both chunks have similar meanings, but chunk in sentence 1 is more specific.
SPE2: like SPE1, but it is the chunk in sentence 2 which is more specific.
SIMI: both chunks have similar meanings, they share similar attributes and there is no EQUI, OPPO, SPE1 or SPE2 relation
REL: both chunks are not considered similar but they are closely related by some relation not mentioned above (i.e. no EQUI, OPPO, SPE1, SPE2, or SIMI relation).
NOALI: this chunk has not any corresponding chunk in the other sentence. Therefore, it is left unaligned.
The possible scores are a range from 0 to 5 where 0 means that the chunks are not related and 5 means their meanings are the same in the given context.
A chunk can be aligned to multiple different chunks. Chunks can also be grouped in a relation. If a chunk has no relation to any other chunk, give it a NOALI relation to a an empty chunk.
If there are spelling mistakes do not correct them.
Present the answers in this form:
chunk from first sentence <==> chunk from the second sentence // alignment type // score
"""
thePrompt2 = """You are a machine designed to align chunks from 2 sentences. This means you will be taking each chunk from one sentence and comparing it to every chunk from the other sentence. Choose the chunks with the strongest relation and assign them a relation and a score.
The possible relations are:
EQUI: both chunks have the same meaning
OPPO: the meanings of the chunks are opposite
SPE1: both chunks have similar meanings, but chunk in sentence 1 is more specific.
SPE2: both chunks have similar meanings, but chunk in sentence 2 is more specific.
SIMI: both chunks have similar meanings
REL: both chunks are not considered similar, but they are closely related by some relation not mentioned above
NOALI: this chunk has not any corresponding chunk in the other sentence.
The possible scores are a range from 0 to 5 where 0 means that the chunks are not related and 5 means their meanings are the same in the given context.
A chunk can be aligned with multiple different chunks. Chunks can also be grouped in a relation. If a chunk has no relation to any other chunk, give it a NOALI relation to a chunk "0".
If there are spelling mistakes, do not correct them.
Present the answers in this form:
chunk from first sentence <==> chunk from the second sentence // relation // score // comment
in the comment you can explain your choice."""
def createGPT() -> OpenAI: def create_gpt() -> OpenAI:
client = OpenAI(api_key="Your API key here") """Build an OpenAI client from OPENAI_API_KEY.
return client
The key used to be a placeholder string literal here, which meant the
script could not actually run as committed.
"""
key = os.environ.get("OPENAI_API_KEY")
if not key:
msg = "set OPENAI_API_KEY to run the alignment scripts"
raise RuntimeError(msg)
return OpenAI(api_key=key)
def callApi(client: OpenAI, chunks: str): def call_api(client: OpenAI, chunks: str) -> str:
"""Ask the model to align one pair of chunked sentences."""
response = client.chat.completions.create( response = client.chat.completions.create(
model="gpt-3.5-turbo-1106", model="gpt-3.5-turbo-1106",
messages=[ messages=[
{"role": "user", "content": thePrompt}, {"role": "user", "content": THE_PROMPT},
{"role": "user", "content": chunks}, {"role": "user", "content": chunks},
], ],
) )
return response.choices[0].message.content.strip() return response.choices[0].message.content.strip()
def callApi_examples(client: OpenAI, examples: pd.DataFrame, alignment: str): def call_api_examples(client: OpenAI, examples: pd.DataFrame, alignment: str) -> str:
"""Ask the model to align, few-shot prompted with worked examples."""
user_input = [] user_input = []
assistant_output = [] assistant_output = []
for index, row in examples.iterrows(): for _index, row in examples.iterrows():
chunks = "" chunks = ""
for chunk in row["chunked_sentance1"]: for chunk in row["chunked_sentance1"]:
chunks = chunks + "[ " + chunk + " ] " chunks = chunks + "[ " + chunk + " ] "
@ -78,8 +59,8 @@ def callApi_examples(client: OpenAI, examples: pd.DataFrame, alignment: str):
assistant_output.append(row["alignment_text"]) assistant_output.append(row["alignment_text"])
messages = [] messages = []
messages.append({"role": "user", "content": thePrompt}) messages.append({"role": "user", "content": THE_PROMPT})
for u, a in zip(user_input, assistant_output): for u, a in zip(user_input, assistant_output, strict=True):
messages.append({"role": "user", "content": u}) messages.append({"role": "user", "content": u})
messages.append({"role": "assistant", "content": a}) messages.append({"role": "assistant", "content": a})

407
Programming/NLP/gpt_chunks.py Normal file → Executable file
View File

@ -1,13 +1,20 @@
#!/usr/bin/env python3
"""Orders gpt to chunkify files from task.""" """Orders gpt to chunkify files from task."""
import logging
import os import os
import re import re
from collections.abc import Callable
from pathlib import Path
from openai import OpenAI from openai import OpenAI
from openai.types.chat import ChatCompletion
logger = logging.getLogger(__name__)
def sentence_loop(sentences): def sentence_loop(sentences: list[str]) -> list[str]:
"""Processes a list of sentences, applying chunking and formatting to each. """Chunk and format every sentence in the list.
This function iterates through a list of sentences, processing each one This function iterates through a list of sentences, processing each one
using the `process_sentence` function. It collects the results into a list. using the `process_sentence` function. It collects the results into a list.
@ -15,7 +22,8 @@ def sentence_loop(sentences):
Parameters Parameters
---------- ----------
sentences (list of str): A list of sentences to be processed. sentences : list of str
A list of sentences to be processed.
Returns Returns
------- -------
@ -32,17 +40,19 @@ def sentence_loop(sentences):
return chunked_sentences return chunked_sentences
def remove_leading_numbering(input_string): def remove_leading_numbering(input_string: str) -> str:
"""Removes any leading numbering from the string. """Remove any leading numbering from the string.
This function is designed to strip off numbering This function is designed to strip off numbering
that typically appears at the beginning of a string, that typically appears at the beginning of a string,
such as '1. ', '2. ', etc. It's useful for processing strings where such numbering is irrelevant such as '1. ', '2. ', etc. It's useful for processing strings where such numbering
is irrelevant
or should be omitted for formatting purposes. or should be omitted for formatting purposes.
Parameters Parameters
---------- ----------
string (str): The string from which leading numbering should be removed. input_string : str
string (str): The string from which leading numbering should be removed.
Returns Returns
------- -------
@ -52,10 +62,11 @@ def remove_leading_numbering(input_string):
return re.sub(r"^\d+\.\s*", "", input_string) return re.sub(r"^\d+\.\s*", "", input_string)
def process_brackets(input_string): def process_brackets(input_string: str) -> list[str]:
"""Processes a string to extract and adjust parts enclosed in square brackets. """Extract and adjust the parts enclosed in square brackets.
This function identifies and extracts all segments within square brackets from the input string. This function identifies and extracts all segments within square brackets from the
input string.
It also adjusts the last part by handling any trailing punctuation, It also adjusts the last part by handling any trailing punctuation,
using the 'handle_trailing_punctuation' function. using the 'handle_trailing_punctuation' function.
This is particularly useful for strings This is particularly useful for strings
@ -63,11 +74,13 @@ def process_brackets(input_string):
Parameters Parameters
---------- ----------
input_string (str): The string to be processed for bracketed segments. input_string : str
The string to be processed for bracketed segments.
Returns Returns
------- -------
list of str: A list containing the extracted and adjusted bracketed segments from the string. list of str: A list containing the extracted and adjusted bracketed segments from
the string.
""" """
parts = re.findall(r"\[[^\]]*\]", input_string) parts = re.findall(r"\[[^\]]*\]", input_string)
@ -76,18 +89,21 @@ def process_brackets(input_string):
return parts return parts
def handle_trailing_punctuation(input_string, last_part): def handle_trailing_punctuation(input_string: str, last_part: str) -> str:
"""Handles and appends trailing punctuation to the last part of a bracketed segment. """Handle and appends trailing punctuation to the last part of a bracketed segment.
This function checks the given string for any punctuation (like '.', '!', or '?') This function checks the given string for any punctuation (like '.', '!', or '?')
immediately following the last bracketed segment. If found, it appends this punctuation immediately following the last bracketed segment. If found, it appends this
punctuation
to the end of the last part inside the brackets. This is used to maintain sentence to the end of the last part inside the brackets. This is used to maintain sentence
punctuation integrity after processing bracketed segments. punctuation integrity after processing bracketed segments.
Parameters Parameters
---------- ----------
input_string (str): The original string containing the bracketed segments. input_string : str
last_part (str): The last bracketed segment extracted from the string. The original string containing the bracketed segments.
last_part : str
The last bracketed segment extracted from the string.
Returns Returns
------- -------
@ -100,32 +116,38 @@ def handle_trailing_punctuation(input_string, last_part):
return last_part return last_part
def process_non_bracketed_parts(input_string): def process_non_bracketed_parts(input_string: str) -> list[str]:
"""Split the input string by hyphens surrounded by optional whitespace and return """Split the input string by hyphens surrounded by optional whitespace and return.
a list of non-empty parts enclosed in square brackets. a list of non-empty parts enclosed in square brackets.
Parameters Parameters
---------- ----------
input_string (str): The input string to be processed. input_string : str
The input string to be processed.
Returns Returns
------- -------
list of str: A list of non-empty parts from the input string, enclosed in square brackets. list of str: A list of non-empty parts from the input string, enclosed in square
brackets.
""" """
parts = re.split(r"\s*-\s*", input_string) parts = re.split(r"\s*-\s*", input_string)
return [f"[ {part.strip()} ]" for part in parts if part.strip()] return [f"[ {part.strip()} ]" for part in parts if part.strip()]
def reformat_list(strings): def reformat_list(strings: list[str]) -> str:
"""Reformat a list of strings by processing each string element. For strings containing """Reformat each string in the list, bracketed or not.
square brackets, it calls 'process_brackets' to handle the content within the brackets,
square brackets, it calls 'process_brackets' to handle the content within the
brackets,
and for other strings, it calls 'process_non_bracketed_parts' to handle them. The and for other strings, it calls 'process_non_bracketed_parts' to handle them. The
resulting parts are joined into a single string with spaces. resulting parts are joined into a single string with spaces.
Parameters Parameters
---------- ----------
strings (list of str): A list of strings to be reformatted. strings : list of str
A list of strings to be reformatted.
Returns Returns
------- -------
@ -133,8 +155,8 @@ def reformat_list(strings):
""" """
result = [] result = []
for string in strings: for raw in strings:
string = remove_leading_numbering(string) string = remove_leading_numbering(raw)
if "[" in string and "]" in string: if "[" in string and "]" in string:
result.extend(process_brackets(string)) result.extend(process_brackets(string))
else: else:
@ -142,8 +164,9 @@ def reformat_list(strings):
return " ".join(result) return " ".join(result)
def reformat_square_not_full(input_string): def reformat_square_not_full(input_string: str) -> str:
"""Reformat the input string by splitting it into parts that are either enclosed in """Reformat the input string by splitting it into parts that are either enclosed in.
square brackets or not. For parts not in brackets, it removes trailing periods, square brackets or not. For parts not in brackets, it removes trailing periods,
trims spaces, and encloses them in square brackets. For parts already in brackets, trims spaces, and encloses them in square brackets. For parts already in brackets,
it adds spaces inside the brackets. The formatted parts are then joined into a it adds spaces inside the brackets. The formatted parts are then joined into a
@ -151,7 +174,8 @@ def reformat_square_not_full(input_string):
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -167,8 +191,8 @@ def reformat_square_not_full(input_string):
# For parts not in brackets, # For parts not in brackets,
# remove trailing period, trim spaces, and enclose in brackets # remove trailing period, trim spaces, and enclose in brackets
if not part.startswith("["): if not part.startswith("["):
part = re.sub(r"\.\s*$", "", part).strip() trimmed = re.sub(r"\.\s*$", "", part).strip()
formatted_parts.append(f"[ {part} ]") formatted_parts.append(f"[ {trimmed} ]")
else: else:
# For parts already in brackets, add spaces inside # For parts already in brackets, add spaces inside
formatted_parts.append(re.sub(r"\[([^]]+)\]", r"[ \1 ]", part)) formatted_parts.append(re.sub(r"\[([^]]+)\]", r"[ \1 ]", part))
@ -176,14 +200,16 @@ def reformat_square_not_full(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def reformat_slash(input_string): def reformat_slash(input_string: str) -> str:
"""Reformat the input string by splitting it on slashes, trimming spaces from each """Reformat the input string by splitting it on slashes, trimming spaces from each.
part, and enclosing each part in square brackets. The formatted parts are then part, and enclosing each part in square brackets. The formatted parts are then
joined into a single string with spaces. joined into a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -197,14 +223,16 @@ def reformat_slash(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def reformat_pipe(input_string): def reformat_pipe(input_string: str) -> str:
"""Reformat the input string by splitting it on ' | ' (pipe symbol with spaces), """Reformat the input string by splitting it on ' | ' (pipe symbol with spaces),.
trimming spaces from each part, and enclosing each part in square brackets. The trimming spaces from each part, and enclosing each part in square brackets. The
formatted parts are then joined into a single string with spaces. formatted parts are then joined into a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -218,12 +246,13 @@ def reformat_pipe(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def is_pipe_format(input_string): def is_pipe_format(input_string: str) -> bool:
"""Check if the input string follows the ' | ' (pipe symbol with spaces) pattern. """Check if the input string follows the ' | ' (pipe symbol with spaces) pattern.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -235,14 +264,13 @@ def is_pipe_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def is_numbered_line_format(input_string): def is_numbered_line_format(input_string: str | list[str]) -> bool | list[bool]:
"""Check if the input string(input_string) follows the pattern of lines starting with r"""Check for lines of the form "number. word/phrase".
'number. word/phrase \n'.
Parameters Parameters
---------- ----------
input_string (str or list of str): The input string(input_string) to be checked. input_string : str or list of str
It can be a single string or a list of strings. The input
Returns Returns
------- -------
@ -258,21 +286,23 @@ def is_numbered_line_format(input_string):
# Check if the input is a list of strings # Check if the input is a list of strings
if isinstance(input_string, list): if isinstance(input_string, list):
return [bool(re.match(pattern, string)) for string in string] return [bool(re.match(pattern, item)) for item in input_string]
# If the input is a single string, process it directly # If the input is a single string, process it directly
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def reformat_ists(input_string): def reformat_ists(input_string: str) -> str:
"""Reformat the input string by finding all occurrences of the (iSTS n) pattern """Reformat the input string by finding all occurrences of the (iSTS n) pattern.
(where 'n' is a number), extracting the text inside the parentheses, and (where 'n' is a number), extracting the text inside the parentheses, and
enclosing each extracted part in square brackets. The formatted parts are enclosing each extracted part in square brackets. The formatted parts are
then joined into a single string with spaces. then joined into a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -287,13 +317,15 @@ def reformat_ists(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def is_ists_format(input_string): def is_ists_format(input_string: str) -> bool:
"""Check if the input string follows the pattern of (iSTS n) followed by text, with """Check if the input string follows the pattern of (iSTS n) followed by text, with.
multiple occurrences allowed, where 'n' is a number. multiple occurrences allowed, where 'n' is a number.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -305,15 +337,17 @@ def is_ists_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def reformat_with_sections(input_string): def reformat_with_sections(input_string: str) -> str:
"""Reformat the input string by using regular expressions to split it into parts """Reformat the input string by using regular expressions to split it into parts.
following section labels (e.g., [S1], [S2], etc.), removing empty strings, following section labels (e.g., [S1], [S2], etc.), removing empty strings,
and enclosing each part in square brackets. The formatted parts are then and enclosing each part in square brackets. The formatted parts are then
joined into a single string with spaces. joined into a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -327,13 +361,15 @@ def reformat_with_sections(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def is_section_format(input_string): def is_section_format(input_string: str) -> bool:
"""Check if the input string follows the pattern of section labels (e.g., [S1], [S2], etc.) """Check for section labels such as [S1] or [S2].
followed by text, with multiple occurrences allowed. followed by text, with multiple occurrences allowed.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -345,13 +381,15 @@ def is_section_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def is_chunk_format(input_string): def is_chunk_format(input_string: str) -> bool:
"""Check if the input string follows the pattern of 'Chunk X: text' or '[Chunk X] text' """Check for the "Chunk X: text" or "[Chunk X] text" pattern.
patterns, where 'X' is a number, with multiple occurrences allowed. patterns, where 'X' is a number, with multiple occurrences allowed.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -359,19 +397,23 @@ def is_chunk_format(input_string):
""" """
# Further optimized regular expression # Further optimized regular expression
pattern = r"^(Chunk \d+:|\[\s*Chunk \d+\s*\]).*?(?:\s+(?:Chunk \d+:|\[\s*Chunk \d+\s*\]).*?)*$" marker = r"(?:Chunk \d+:|\[\s*Chunk \d+\s*\])"
pattern = rf"^{marker}.*?(?:\s+{marker}.*?)*$"
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def reformat_chunks(input_string): def reformat_chunks(input_string: str) -> str:
"""Reformat the input string by finding all occurrences of both """Reformat the input string by finding all occurrences of both.
'Chunk X: text' and '[Chunk X] text' 'Chunk X: text' and '[Chunk X] text'
patterns, extracting the text within them, and enclosing each extracted part in square brackets. patterns, extracting the text within them, and enclosing each extracted part in
square brackets.
The formatted parts are then joined into a single string with spaces. The formatted parts are then joined into a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -379,8 +421,9 @@ def reformat_chunks(input_string):
""" """
# Find all occurrences of both chunk patterns and extract the text # Find all occurrences of both chunk patterns and extract the text
marker = r"(?:Chunk \d+:|\[\s*Chunk \d+\s*\])"
parts = re.findall( parts = re.findall(
r"(?:Chunk \d+:|\[\s*Chunk \d+\s*\])\s*(.*?)(?=\s*(?:Chunk \d+:|\[\s*Chunk \d+\s*\]|$))", rf"{marker}\s*(.*?)(?=\s*(?:{marker}|$))",
input_string, input_string,
) )
@ -392,14 +435,16 @@ def reformat_chunks(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def reformat_ists_markers(input_string): def reformat_ists_markers(input_string: str) -> str:
"""Reformat the input string by splitting it at '[ iSTS ]' markers, removing empty strings, """Reformat a string split at its "[ iSTS ]" markers.
and enclosing each part in square brackets. The formatted parts are then joined into and enclosing each part in square brackets. The formatted parts are then joined into
a single string with spaces. a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -417,13 +462,15 @@ def reformat_ists_markers(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def is_ists_marker_format(input_string): def is_ists_marker_format(input_string: str) -> bool:
"""Check if the input string follows the pattern with '[ iSTS ]' markers followed by text, """Check for "[ iSTS ]" markers followed by text.
with multiple occurrences allowed. with multiple occurrences allowed.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -435,14 +482,16 @@ def is_ists_marker_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def reformat_with_dashes(input_string): def reformat_with_dashes(input_string: str) -> str:
"""Reformat the input string by splitting it on ' - ' (dash with spaces), """Reformat the input string by splitting it on ' - ' (dash with spaces),.
removing empty strings, and enclosing each part in square brackets. removing empty strings, and enclosing each part in square brackets.
The formatted parts are then joined into a single string with spaces. The formatted parts are then joined into a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -456,13 +505,15 @@ def reformat_with_dashes(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def is_dash_format(input_string): def is_dash_format(input_string: str) -> bool:
"""Check if the input string follows the pattern with ' - ' (dash with spaces) """Check if the input string follows the pattern with ' - ' (dash with spaces).
separating non-empty parts. separating non-empty parts.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -474,8 +525,9 @@ def is_dash_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def preliminary_reformat(input_string): def preliminary_reformat(input_string: str) -> str:
"""Remove common prefixes like """Remove common prefixes like.
'iSTS chunks:' 'iSTS chunks:'
and and
'Here are the iSTS chunks for the given sentence:' 'Here are the iSTS chunks for the given sentence:'
@ -484,29 +536,34 @@ def preliminary_reformat(input_string):
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
str: The input string with common prefixes stripped. str: The input string with common prefixes stripped.
""" """
input_string = input_string.strip("iSTS chunks:") # removeprefix, not strip: str.strip takes a SET OF CHARACTERS, so
input_string = input_string.strip( # .strip("iSTS chunks:") was eating any leading i, S, T, s, c, h, u, n, k,
"Here are the iSTS chunks for the given sentence:" # space or colon -- it turned "sentence one" into "entence one".
) input_string = input_string.removeprefix("iSTS chunks:")
return input_string return input_string.removeprefix("Here are the iSTS chunks for the given sentence:")
def reformat_brackets_and_text(input_string): def reformat_brackets_and_text(input_string: str) -> str:
"""Reformat the input string by removing trailing dots, splitting it into parts that are either """Reformat a dash-separated string into bracketed chunks.
enclosed in square brackets or not, removing commas and trimming spaces for non-bracket parts,
and enclosing non-bracket parts in square brackets. The formatted parts are then joined into enclosed in square brackets or not, removing commas and trimming spaces for non-
bracket parts,
and enclosing non-bracket parts in square brackets. The formatted parts are then
joined into
a single string with spaces. a single string with spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
@ -524,9 +581,9 @@ def reformat_brackets_and_text(input_string):
if part: # Ignore empty strings if part: # Ignore empty strings
# Remove commas and trim spaces for parts not in brackets # Remove commas and trim spaces for parts not in brackets
if not part.startswith("["): if not part.startswith("["):
part = re.sub(r",\s*", " ", part).strip() trimmed = re.sub(r",\s*", " ", part).strip()
# Enclose the non-bracket part in brackets # Enclose the non-bracket part in brackets
formatted_parts.append(f"[ {part} ]") formatted_parts.append(f"[ {trimmed} ]")
else: else:
# Directly append the part in brackets # Directly append the part in brackets
formatted_parts.append(part) formatted_parts.append(part)
@ -534,14 +591,16 @@ def reformat_brackets_and_text(input_string):
return " ".join(formatted_parts) return " ".join(formatted_parts)
def is_brackets_and_text_format(input_string): def is_brackets_and_text_format(input_string: str) -> bool:
"""Check if the input string follows the pattern of parts """Check if the input string follows the pattern of parts.
that are either enclosed in square brackets that are either enclosed in square brackets
or not, with optional commas and spaces between the parts. or not, with optional commas and spaces between the parts.
Parameters Parameters
---------- ----------
input_string (str): The input string to be checked. input_string : str
The input string to be checked.
Returns Returns
------- -------
@ -553,16 +612,19 @@ def is_brackets_and_text_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def is_correct_format(input_string): def is_correct_format(input_string: str) -> bool:
"""Checks if a given string matches a specific format of sequences enclosed in square brackets. """Check that the string is a sequence of bracketed chunks.
This function uses a regular expression to determine if the input string adheres to a format This function uses a regular expression to determine if the input string adheres to
where sequences of text are enclosed within square brackets. Each sequence must be separated a format
where sequences of text are enclosed within square brackets. Each sequence must be
separated
by spaces, and the entire string must only consist of these bracketed sequences. by spaces, and the entire string must only consist of these bracketed sequences.
Parameters Parameters
---------- ----------
input_string (str): The string to be checked against the format. input_string : str
The string to be checked against the format.
Returns Returns
------- -------
@ -576,20 +638,20 @@ def is_correct_format(input_string):
return bool(re.match(pattern, input_string)) return bool(re.match(pattern, input_string))
def check_format(input_data): def check_format(input_data: str | list[str]) -> bool | list[bool] | None:
"""Checks if the input data (input_data or list of strings) matches a specific format. """Check one string, or every string in a list, for the chunk format.
This function determines if the input data, This function determines if the input data,
which can be either a single string or a list of strings, which can be either a single string or a list of strings,
adheres to a predefined format where sequences of text are enclosed within square brackets. adheres to a predefined format where sequences of text are enclosed within square
brackets.
The format check is performed by the 'is_correct_format' function. The format check is performed by the 'is_correct_format' function.
For a list, the check is applied to each string in the list. For a list, the check is applied to each string in the list.
Parameters Parameters
---------- ----------
input_data (str or list of str): input_data : str or list of str
The input data to be checked. The input data to be checked. Can be a single string or a list of strings.
Can be a single string or a list of strings.
Returns Returns
------- -------
@ -611,26 +673,33 @@ def check_format(input_data):
return None return None
def check_and_reformat(input_string, check_func, format_func): def check_and_reformat(
"""Checks what type of string got inputed and formats it accordingly.""" input_string: str,
check_func: Callable[[str], bool],
format_func: Callable[[str], str],
) -> str | None:
"""Check what type of string got inputed and formats it accordingly."""
if check_func(input_string): if check_func(input_string):
print(f"formatted by {format_func.__name__}", input_string) logger.info("formatted by %s %s", format_func.__name__, input_string)
return format_func(input_string) return format_func(input_string)
return None return None
def reformat(input_string): def reformat(input_string: str) -> str | bool:
"""Reformat the input string by applying a series of format checks and corresponding """Identify the input's format and apply the matching reformatter.
reformatting functions. This function attempts to identify the format of the input reformatting functions. This function attempts to identify the format of the input
string and apply the appropriate reformatting. string and apply the appropriate reformatting.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
str or bool: The reformatted string if a suitable format is recognized, or False if the format str or bool: The reformatted string if a suitable format is recognized, or False if
the format
is not recognized and cannot be reformatted. is not recognized and cannot be reformatted.
""" """
@ -656,17 +725,21 @@ def reformat(input_string):
if result is not None: if result is not None:
return result return result
print("ERROR! reformat did not recognize the format of string! ", input_string) logger.info(
"ERROR! reformat did not recognize the format of string! %s", input_string
)
return False return False
def remove_empty_brackets(input_string): def remove_empty_brackets(input_string: str) -> str:
"""Remove empty brackets (with or without spaces) from the input string and """Remove empty brackets (with or without spaces) from the input string and.
eliminate any extra spaces that might be left after removing brackets. eliminate any extra spaces that might be left after removing brackets.
Parameters Parameters
---------- ----------
input_string (str): The input string from which empty brackets should be removed. input_string : str
The input string from which empty brackets should be removed.
Returns Returns
------- -------
@ -680,35 +753,35 @@ def remove_empty_brackets(input_string):
cleaned_string = re.sub(pattern, "", input_string) cleaned_string = re.sub(pattern, "", input_string)
# Remove any extra spaces that might be left after removing brackets # Remove any extra spaces that might be left after removing brackets
cleaned_string = re.sub(r"\s{2,}", " ", cleaned_string).strip() return re.sub(r"\s{2,}", " ", cleaned_string).strip()
return cleaned_string
def reformat_brackets(input_string): def reformat_brackets(input_string: str) -> str:
"""Reformat the input string by enclosing text inside square brackets. """Reformat the input string by enclosing text inside square brackets.
This function searches for text enclosed in square brackets (with or without spaces) This function searches for text enclosed in square brackets (with or without spaces)
and ensures that the text inside the brackets is surrounded by spaces. and ensures that the text inside the brackets is surrounded by spaces.
Parameters Parameters
---------- ----------
input_string (str): The input string to be reformatted. input_string : str
The input string to be reformatted.
Returns Returns
------- -------
str: A reformatted string with text inside square brackets and spaces around it. str: A reformatted string with text inside square brackets and spaces around it.
""" """
formatted_string = re.sub(r"\[\s*(.*?)\s*\]", r"[ \1 ]", input_string) return re.sub(r"\[\s*(.*?)\s*\]", r"[ \1 ]", input_string)
return formatted_string
def generate_instruction(sentence): def generate_instruction(sentence: str) -> str:
"""Generate an instruction for dividing a given sentence into iSTS chunks. """Generate an instruction for dividing a given sentence into iSTS chunks.
Parameters Parameters
---------- ----------
sentence (str): The sentence to be divided into chunks. sentence : str
The sentence to be divided into chunks.
Returns Returns
------- -------
@ -723,13 +796,13 @@ def generate_instruction(sentence):
) )
def call_api(instruction): def call_api(instruction: str) -> ChatCompletion:
"""Call the API using the provided instruction to generate a response. """Call the API using the provided instruction to generate a response.
Parameters Parameters
---------- ----------
client (OpenAI.Client): An OpenAI API client object. instruction : str
instruction (str): The instruction to provide to the API. The instruction to provide to the API.
Returns Returns
------- -------
@ -747,28 +820,29 @@ def call_api(instruction):
) )
def process_response(response): def process_response(response: ChatCompletion) -> str:
"""Process the response received from the API and extract the chunked sentence. """Process the response received from the API and extract the chunked sentence.
Parameters Parameters
---------- ----------
response (dict): The response generated by the API. response : dict
The response generated by the API.
Returns Returns
------- -------
str: The chunked sentence extracted from the response. str: The chunked sentence extracted from the response.
""" """
chunked_sentence = response.choices[0].message.content.strip() return response.choices[0].message.content.strip()
return chunked_sentence
def check_and_format_sentence(chunked_sentence): def check_and_format_sentence(chunked_sentence: str) -> str | bool:
"""Check and format a chunked sentence by applying a series of checks and reformatting. """Check a chunked sentence and reformat it if it needs it.
Parameters Parameters
---------- ----------
chunked_sentence (str): The chunked sentence to be checked and formatted. chunked_sentence : str
The chunked sentence to be checked and formatted.
Returns Returns
------- -------
@ -778,22 +852,24 @@ def check_and_format_sentence(chunked_sentence):
""" """
reformated = reformat(chunked_sentence) reformated = reformat(chunked_sentence)
if not reformated: if not reformated:
print("ERROR! failed to reformat! ", chunked_sentence, reformated) logger.info("ERROR! failed to reformat! %s %s", chunked_sentence, reformated)
return False return False
reformated = reformated.strip(",") reformated = reformated.strip(",")
if not is_correct_format(reformated): if not is_correct_format(reformated):
print("ERROR! wrong format ", chunked_sentence, reformated) logger.info("ERROR! wrong format %s %s", chunked_sentence, reformated)
return False return False
return reformated return reformated
def finalize_formatting(reformatted): def finalize_formatting(reformatted: str) -> str:
"""Finalize the formatting of a reformatted string by removing empty brackets and ensuring """Finalise a reformatted string: drop empty brackets, tidy spacing.
that text inside brackets is surrounded by spaces. that text inside brackets is surrounded by spaces.
Parameters Parameters
---------- ----------
reformatted (str): The reformatted string to be finalized. reformatted : str
The reformatted string to be finalized.
Returns Returns
------- -------
@ -801,24 +877,26 @@ def finalize_formatting(reformatted):
""" """
reformatted = remove_empty_brackets(reformatted) reformatted = remove_empty_brackets(reformatted)
reformatted = reformat_brackets(reformatted) return reformat_brackets(reformatted)
return reformatted
def process_sentence(sentence, chunked_sentences): def process_sentence(sentence: str, chunked_sentences: list[str]) -> list[str] | bool:
"""Process a sentence by generating iSTS chunks and """Process a sentence by generating iSTS chunks and.
adding them to the provided list of chunked sentences. adding them to the provided list of chunked sentences.
Parameters Parameters
---------- ----------
sentence (str): The input sentence to be processed. sentence : str
chunked_sentences (list): A list of previously chunked sentences to which the processed sentence The input sentence to be processed.
will be appended. chunked_sentences : list
client (OpenAI.Client): An OpenAI API client object for calling the API. A list of previously chunked sentences to which the processed sentence will be
appended.
Returns Returns
------- -------
list or bool: The updated list of chunked sentences if successful, or False if there are errors. list or bool: The updated list of chunked sentences if successful, or False if there
are errors.
""" """
instruction = generate_instruction(sentence) instruction = generate_instruction(sentence)
@ -832,14 +910,21 @@ def process_sentence(sentence, chunked_sentences):
return chunked_sentences return chunked_sentences
def chunk_sentences(input_file, output): def chunk_sentences(input_file: str, output: str) -> None:
"""Read sentences from a file, process each sentence to generate iSTS chunks, """Read sentences from a file, process each sentence to generate iSTS chunks,.
and write the chunked sentences to an output file. and write the chunked sentences to an output file.
Parameters Parameters
---------- ----------
file_path (str): The path to the input file containing sentences to be processed. input_file : str
output_path (str): The path to the output file where chunked sentences will be written. file_path (str): The path to the input file containing sentences to be
processed. output_path (str): The path to the output file where chunked
sentences will be written.
output : str
file_path (str): The path to the input file containing sentences to be
processed. output_path (str): The path to the output file where chunked
sentences will be written.
Returns Returns
------- -------
@ -847,15 +932,15 @@ def chunk_sentences(input_file, output):
""" """
# Read the sentences from the file # Read the sentences from the file
with open(input_file, encoding="utf-8") as file: with Path(input_file).open(encoding="utf-8") as file:
sentences = file.readlines() sentences = file.readlines()
# Process each sentence # Process each sentence
chunked_sentences = sentence_loop(sentences) chunked_sentences = sentence_loop(sentences)
print(chunked_sentences) logger.info("%s", chunked_sentences)
# Write the chunked sentences to a new file # Write the chunked sentences to a new file
with open(output, "w", encoding="utf-8") as output_file: with Path(output).open("w", encoding="utf-8") as output_file:
output_file.writelines(sentence + "\n" for sentence in chunked_sentences) output_file.writelines(sentence + "\n" for sentence in chunked_sentences)
@ -882,6 +967,6 @@ output_files = [
# Change me to os.environ['API_KEY'] # Change me to os.environ['API_KEY']
client = OpenAI(api_key=os.environ["API_KEY"]) client = OpenAI(api_key=os.environ["API_KEY"])
for input_path, output_path in zip(input_files, output_files): for input_path, output_path in zip(input_files, output_files, strict=False):
chunk_sentences(input_path, output_path) chunk_sentences(input_path, output_path)
print("FINISHED GPT CHUNKS FOR FILE: ", input_path) logger.info("FINISHED GPT CHUNKS FOR FILE: %s", input_path)

75
Programming/NLP/processing.py Normal file → Executable file
View File

@ -1,24 +1,30 @@
#!/usr/bin/env python3
"""Load the SemEval corpus: sentences, chunkings and gold alignments."""
import logging
import re import re
from pathlib import Path
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from lxml import etree from lxml import etree
logger = logging.getLogger(__name__)
def load_sentences(senteance1_path: str, sentance2_path: str) -> pd.DataFrame: def load_sentences(senteance1_path: str, sentance2_path: str) -> pd.DataFrame:
"""Loads the sentences from the given paths and outputs in a 2 columns dataframe.""" """Load the sentences from the given paths and outputs in a 2 columns dataframe."""
sentance1 = pd.read_csv(senteance1_path, dtype=str, delimiter="}", header=None) sentance1 = pd.read_csv(senteance1_path, dtype=str, delimiter="}", header=None)
sentance1.columns = ["sentance1"] sentance1.columns = ["sentance1"]
sentance2 = pd.read_csv(sentance2_path, dtype=str, delimiter="}", header=None) sentance2 = pd.read_csv(sentance2_path, dtype=str, delimiter="}", header=None)
sentance2.columns = ["sentance2"] sentance2.columns = ["sentance2"]
sentances = pd.concat([sentance1, sentance2], axis=1) return pd.concat([sentance1, sentance2], axis=1)
return sentances
def chunk2list(chunks: str) -> list: def chunk2list(chunks: str) -> list:
"""Takes str that is all chunks from a chunked sentance and returns a list of all the chunks as seperate items.""" """Split one chunked sentence into its chunks."""
chunks = chunks.replace("[", "") chunks = chunks.replace("[", "")
chunks = chunks.replace("]", "") chunks = chunks.replace("]", "")
chunks = chunks.replace(" ", "|") chunks = chunks.replace(" ", "|")
@ -27,12 +33,14 @@ def chunk2list(chunks: str) -> list:
split = [ split = [
re.sub(r"^\s+|\s+$", "", s) for s in split re.sub(r"^\s+|\s+$", "", s) for s in split
] # remove spaces at the beggining and end of chunks ] # remove spaces at the beggining and end of chunks
split = [re.sub(r"[^\w\s]", "", s) for s in split] # remove punctuation return [re.sub(r"[^\w\s]", "", s) for s in split] # remove punctuation
return split
def load_chunked(chunked_path1: str, chunked_path2: str) -> pd.DataFrame: def load_chunked(chunked_path1: str, chunked_path2: str) -> pd.DataFrame:
"""Loads chunked sentances in [ chunk1 ] [ chunk2 ] format into dataframe with lists of chunks.""" """Load two chunk files into a dataframe of chunk lists.
The on-disk format is ``[ chunk1 ] [ chunk2 ] ...`` per line.
"""
chunked_sentance1 = pd.read_csv( chunked_sentance1 = pd.read_csv(
chunked_path1, dtype=str, delimiter="}", header=None chunked_path1, dtype=str, delimiter="}", header=None
) )
@ -57,15 +65,18 @@ def load_chunked(chunked_path1: str, chunked_path2: str) -> pd.DataFrame:
def return_characteers(cell: str) -> str: def return_characteers(cell: str) -> str:
"""Converts the alignment data to restore the <==> and & tokens.""" """Convert the alignment data to restore the <==> and & tokens."""
cell = cell.replace("ARROWS_PLACEHOLDER", "<==>") cell = cell.replace("ARROWS_PLACEHOLDER", "<==>")
cell = cell.replace("AMPERSAND_PLACEHOLDER", "&") return cell.replace("AMPERSAND_PLACEHOLDER", "&")
return cell
def load_alignment(alignment_path: str) -> pd.DataFrame: def load_alignment(alignment_path: str) -> pd.DataFrame:
"""Loads the alignment file. Parses only the <alignment> tag and puts the data into a dataframe.""" """Load the gold alignment file into a dataframe.
with open(alignment_path) as file:
Only the ``<alignment>`` tag is parsed; the rest of the .wa XML is
metadata this project does not use.
"""
with Path(alignment_path).open() as file:
file_content = file.read() file_content = file.read()
# <==> and & break xml loaders so it needs to be replaces with something else # <==> and & break xml loaders so it needs to be replaces with something else
@ -76,7 +87,7 @@ def load_alignment(alignment_path: str) -> pd.DataFrame:
modified_content = f"<root>{modified_content}</root>" modified_content = f"<root>{modified_content}</root>"
modified_file_path = "temp.wa" modified_file_path = "temp.wa"
with open(modified_file_path, "w") as modified_file: with Path(modified_file_path).open("w") as modified_file:
modified_file.write(modified_content) modified_file.write(modified_content)
# Parse the modified file using ElementTree # Parse the modified file using ElementTree
@ -100,19 +111,29 @@ def load_alignment(alignment_path: str) -> pd.DataFrame:
return y return y
def prettyprint(element, **kwargs): def prettyprint(element: etree._Element, **kwargs: object) -> None:
"""Log an XML element with lxml's pretty printer."""
xml = etree.tostring(element, pretty_print=True, **kwargs) xml = etree.tostring(element, pretty_print=True, **kwargs)
print(xml.decode(), end="") logger.info("%s", xml.decode())
def test_XML(): def log_first_alignment(alignments_data: list[dict[str, str]]) -> None:
# test out the format """Log the first parsed alignment, to eyeball the .wa parsing.
print(alignments_data[0]["alignment_text"])
This was `test_XML()` and read a module-level `alignments_data` that does
not exist at module scope -- it only ever worked when pasted into a
session where load_alignment had already run. It takes the data as an
argument now, and is no longer named test_* so pytest does not collect it.
"""
logger.info("%s", alignments_data[0]["alignment_text"])
def generate_train_test_split(xy: pd.DataFrame): def generate_train_test_split(xy: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Generates a train, validate, test split of the given dataframes in a 60% 20% 20% ratio.""" """Split the dataframe into a train and a test half.
# data = pd.merge(x, y, left_index=True, right_index=True)
The docstring said 60/20/20 when it was written; the code splits at 3%,
which is what the experiments actually used.
"""
data = xy data = xy
train, test = np.split( train, test = np.split(
data.sample(frac=1, random_state=42), [int(0.03 * len(data))] data.sample(frac=1, random_state=42), [int(0.03 * len(data))]
@ -121,21 +142,23 @@ def generate_train_test_split(xy: pd.DataFrame):
return train, test return train, test
def generate_alignment_format(dataFrame: pd.DataFrame, id: int) -> str: def generate_alignment_format(data_frame: pd.DataFrame, row_id: int) -> str:
"""Render one row's two chunkings as the numbered text GPT is shown."""
output = "seq1:\n" output = "seq1:\n"
chunks1 = dataFrame["chunked_sentance1"][id] chunks1 = data_frame["chunked_sentance1"][row_id]
for i, chunk in enumerate(chunks1): for i, chunk in enumerate(chunks1):
output = output + str(i + 1) + ") " + str(chunk) + "\n" output = output + str(i + 1) + ") " + str(chunk) + "\n"
output = output + "\nseq2:\n" output = output + "\nseq2:\n"
chunks2 = dataFrame["chunked_sentance2"][id] chunks2 = data_frame["chunked_sentance2"][row_id]
for i, chunk in enumerate(chunks2): for i, chunk in enumerate(chunks2):
output = output + str(i + 1) + ") " + str(chunk) + "\n" output = output + str(i + 1) + ") " + str(chunk) + "\n"
return output return output
def get_chunks_as_text(data: pd.DataFrame) -> str: def get_chunks_as_text(data: pd.DataFrame) -> tuple[list[str], pd.Index]:
"""Render every row's chunk pair as bracketed text, with its index."""
output = [] output = []
for index, row in data.iterrows(): for _index, row in data.iterrows():
chunks = "" chunks = ""
for chunk in row["chunked_sentance1"]: for chunk in row["chunked_sentance1"]:
chunks = chunks + "[ " + chunk + " ] " chunks = chunks + "[ " + chunk + " ] "

View File

@ -0,0 +1,19 @@
You are a machine designed to align chunks from 2 sentences. This means you will be taking each chunk from one sentence and comparing it to every chunk from the other sentence. Choose the chunks with the strongest relation and assign them a score.
The possible relations are:
EQUI: both chunks have the same meaning, they are semantically equivalent in this context.
OPPO: the meanings of the chunks are in opposition to each other, lying in an inherently incompatible binary relationship.
SPE1: both chunks have similar meanings, but chunk in sentence 1 is more specific.
SPE2: like SPE1, but it is the chunk in sentence 2 which is more specific.
SIMI: both chunks have similar meanings, they share similar attributes and there is no EQUI, OPPO, SPE1 or SPE2 relation
REL: both chunks are not considered similar but they are closely related by some relation not mentioned above (i.e. no EQUI, OPPO, SPE1, SPE2, or SIMI relation).
NOALI: this chunk has not any corresponding chunk in the other sentence. Therefore, it is left unaligned.
The possible scores are a range from 0 to 5 where 0 means that the chunks are not related and 5 means their meanings are the same in the given context.
A chunk can be aligned to multiple different chunks. Chunks can also be grouped in a relation. If a chunk has no relation to any other chunk, give it a NOALI relation to a an empty chunk.
If there are spelling mistakes do not correct them.
Present the answers in this form:
chunk from first sentence <==> chunk from the second sentence // alignment type // score

View File

@ -0,0 +1,21 @@
You are a machine designed to align chunks from 2 sentences. This means you will be taking each chunk from one sentence and comparing it to every chunk from the other sentence. Choose the chunks with the strongest relation and assign them a relation and a score.
The possible relations are:
EQUI: both chunks have the same meaning
OPPO: the meanings of the chunks are opposite
SPE1: both chunks have similar meanings, but chunk in sentence 1 is more specific.
SPE2: both chunks have similar meanings, but chunk in sentence 2 is more specific.
SIMI: both chunks have similar meanings
REL: both chunks are not considered similar, but they are closely related by some relation not mentioned above
NOALI: this chunk has not any corresponding chunk in the other sentence.
The possible scores are a range from 0 to 5 where 0 means that the chunks are not related and 5 means their meanings are the same in the given context.
A chunk can be aligned with multiple different chunks. Chunks can also be grouped in a relation. If a chunk has no relation to any other chunk, give it a NOALI relation to a chunk "0".
If there are spelling mistakes, do not correct them.
Present the answers in this form:
chunk from first sentence <==> chunk from the second sentence // relation // score // comment
in the comment you can explain your choice.

41
Programming/NLP/rating.py Normal file → Executable file
View File

@ -1,5 +1,42 @@
#!/usr/bin/env python3
"""Ask the model to rate how similar two chunks are."""
import os
from openai import OpenAI from openai import OpenAI
def rate_similarity(c1: list, c2: list) -> str: def _api_key() -> str:
client = OpenAI(api_key="REDACTED_OPENAI_API_KEY") """Return OPENAI_API_KEY, or explain that it is missing."""
key = os.environ.get("OPENAI_API_KEY")
if not key:
msg = "set OPENAI_API_KEY to run the rating script"
raise RuntimeError(msg)
return key
def rate_similarity(c1: list[str], c2: list[str]) -> str:
"""Rate the similarity of two chunk lists on the course's 0-5 scale.
This was submitted unfinished: it built a client and stopped, so ``c1``
and ``c2`` were never used and the function returned None despite being
annotated ``-> str``. The call below is the one the rest of this package
already makes (see gpt_alignment.call_api); nothing else about the
original is changed.
"""
client = OpenAI(api_key=_api_key())
response = client.chat.completions.create(
model="gpt-3.5-turbo-1106",
messages=[
{
"role": "user",
"content": (
"Rate on a 0-5 scale how similar these two chunk lists are, "
"where 0 is unrelated and 5 is the same meaning. Answer with "
"the number alone.\n"
f"1: {c1}\n2: {c2}"
),
}
],
)
return str(response.choices[0].message.content).strip()

83
Programming/NLP/test_goldStandard/chunks.py Normal file → Executable file
View File

@ -1,50 +1,69 @@
#!/usr/bin/env python3
"""Ask GPT to divide the student-answer sentences into iSTS chunks.
The earlier, simpler sibling of ``../gpt_chunks.py``: no format detection and
no reformatting pass, just one request per sentence with the reply written
straight out. Its output is ``chunks_gpt_two.txt`` next to this file.
The prompt lives in ``prompt.txt`` because it is a single 373-character line
that cannot be wrapped without changing the prompt itself.
"""
import logging
import os
from pathlib import Path
from openai import OpenAI from openai import OpenAI
logger = logging.getLogger(__name__)
def sentence_loop(sentences): PROMPT = (Path(__file__).parent / "prompt.txt").read_text(encoding="utf-8").strip()
# Process each sentence
chunked_sentences = []
def sentence_loop(client: OpenAI, sentences: list[str]) -> list[str]:
"""Chunk every sentence, in order, and collect the replies."""
chunked_sentences: list[str] = []
for sentence in sentences: for sentence in sentences:
chunked_sentences = process_sentence(sentence, chunked_sentences) chunked_sentences = process_sentence(client, sentence, chunked_sentences)
return chunked_sentences return chunked_sentences
def process_sentence(sentence, chunked_sentences): def process_sentence(
client: OpenAI, sentence: str, chunked_sentences: list[str]
) -> list[str]:
"""Chunk one sentence and append the reply to *chunked_sentences*."""
response = client.chat.completions.create( response = client.chat.completions.create(
messages=[ messages=[{"role": "user", "content": PROMPT + sentence}],
{
"role": "user",
"content": "Please divide the following sentence into iSTS chunks. Format the response as: [[chunk 1]] [[chunk 2]] ... Ensure each chunk is enclosed in double square brackets and separated by a space. If you cannot correctly chunkify the sentence, return an error message in the format: 'ERROR: Cannot chunkify sentence: <sentence>'. Here is the sentence: "
+ sentence,
}
],
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
) )
chunked_sentence = str(response.choices[0].message.content)
chunked_sentence = response.choices[0].message.content logger.info("chunked_sentence: %s", chunked_sentence)
print("chunked_sentence: ", chunked_sentence)
chunked_sentences.append(chunked_sentence) chunked_sentences.append(chunked_sentence)
return chunked_sentences return chunked_sentences
def chunk_sentences(file_path, output_path, api_key): def chunk_sentences(file_path: str, output_path: str) -> None:
# Read the sentences from the file """Chunk every sentence in *file_path* and write them to *output_path*."""
with open(file_path) as file: # The key was a literal here with a "Replace me with os.environ" comment
# next to it, which is now done rather than noted.
key = os.environ.get("OPENAI_API_KEY")
if not key:
msg = "set OPENAI_API_KEY to run the chunking script"
raise RuntimeError(msg)
client = OpenAI(api_key=key)
with Path(file_path).open(encoding="utf-8") as file:
sentences = file.readlines() sentences = file.readlines()
# Process each sentence chunked = sentence_loop(client, sentences)
chunk_sentences = sentence_loop(sentences) logger.info("%s", chunked)
print(chunk_sentences) with Path(output_path).open("w", encoding="utf-8") as output_file:
# Write the chunked sentences to a new file output_file.writelines(sentence + "\n" for sentence in chunked)
with open(output_path, "w") as output_file:
output_file.writelines(sentence + "\n" for sentence in chunk_sentences)
# Usage if __name__ == "__main__":
file_path = "student/STSint.testinput.answers-students.sent2.txt" logging.basicConfig(format="%(message)s", level=logging.INFO)
output_path = "chunks_gpt_two.txt" chunk_sentences(
# Replace me with os.environ['API_KEY'] "student/STSint.testinput.answers-students.sent2.txt",
api_key = "REDACTED_OPENAI_API_KEY" "chunks_gpt_two.txt",
client = OpenAI(api_key=api_key) )
chunk_sentences(file_path, output_path, api_key)

View File

@ -0,0 +1 @@
Please divide the following sentence into iSTS chunks. Format the response as: [[chunk 1]] [[chunk 2]] ... Ensure each chunk is enclosed in double square brackets and separated by a space. If you cannot correctly chunkify the sentence, return an error message in the format: 'ERROR: Cannot chunkify sentence: <sentence>'. Here is the sentence: