waterfall 0.1.3__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waterfall
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Scalable Framework for Robust Text Watermarking and Provenance for LLMs
5
5
  Project-URL: Homepage, https://github.com/aoi3142/Waterfall
6
6
  Project-URL: Issues, https://github.com/aoi3142/Waterfall/issues
@@ -141,7 +141,7 @@ from waterfall.watermark import verify_texts
141
141
  id = 1 # specify your watermarking ID
142
142
  test_texts = ["...", "..."] # Suspected texts to verify
143
143
 
144
- watermark_strength = verify_texts(test_texts, id)[0] # np array of floats
144
+ watermark_strength = verify_texts(test_texts, id) # np array of floats
145
145
  ```
146
146
 
147
147
  ## Code structure
@@ -121,7 +121,7 @@ from waterfall.watermark import verify_texts
121
121
  id = 1 # specify your watermarking ID
122
122
  test_texts = ["...", "..."] # Suspected texts to verify
123
123
 
124
- watermark_strength = verify_texts(test_texts, id)[0] # np array of floats
124
+ watermark_strength = verify_texts(test_texts, id) # np array of floats
125
125
  ```
126
126
 
127
127
  ## Code structure
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "waterfall"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  authors = [
9
9
  { name = "Xinyuan Niu", email="aperture@outlook.sg" }
10
10
  ]
@@ -0,0 +1,129 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "ddca610d",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "from waterfall.watermark import *\n",
11
+ "import torch"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": 2,
17
+ "id": "f0b0a02a",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "model_name_or_path = 'meta-llama/Llama-3.1-8B-Instruct'"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": 3,
27
+ "id": "ca5f7d55",
28
+ "metadata": {},
29
+ "outputs": [
30
+ {
31
+ "data": {
32
+ "application/vnd.jupyter.widget-view+json": {
33
+ "model_id": "2ef1e58174204e879a462a2a829d74fa",
34
+ "version_major": 2,
35
+ "version_minor": 0
36
+ },
37
+ "text/plain": [
38
+ "Loading checkpoint shards: 0%| | 0/4 [00:00<?, ?it/s]"
39
+ ]
40
+ },
41
+ "metadata": {},
42
+ "output_type": "display_data"
43
+ }
44
+ ],
45
+ "source": [
46
+ "\n",
47
+ "tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)\n",
48
+ "model = AutoModelForCausalLM.from_pretrained(\n",
49
+ " model_name_or_path,\n",
50
+ " torch_dtype=torch.bfloat16,\n",
51
+ " device_map=\"cuda:5\",\n",
52
+ " )"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": 4,
58
+ "id": "2c475de5",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "watermarker = Watermarker(tokenizer=tokenizer, model=model, id=1)"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": 5,
68
+ "id": "e74a58b5",
69
+ "metadata": {},
70
+ "outputs": [],
71
+ "source": [
72
+ "texts = [\"James is a scary man who loves to scare children\", \"There were 3 birds that ate the whole bowl\"]"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": 8,
78
+ "id": "fd0af01e",
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "watermarked_text = watermark_texts(texts, id, watermarker=watermarker)"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "code",
87
+ "execution_count": 10,
88
+ "id": "4d58ec82",
89
+ "metadata": {},
90
+ "outputs": [
91
+ {
92
+ "data": {
93
+ "text/plain": [
94
+ "['James is an intimidating individual known for taking great pleasure in frightening kids, and this tendency often sends them running in terror.',\n",
95
+ " \"A small flock comprised of three birds consumed the entirety of the offered bowl's contents within a single meal session.\"]"
96
+ ]
97
+ },
98
+ "execution_count": 10,
99
+ "metadata": {},
100
+ "output_type": "execute_result"
101
+ }
102
+ ],
103
+ "source": [
104
+ "watermarked_text"
105
+ ]
106
+ }
107
+ ],
108
+ "metadata": {
109
+ "kernelspec": {
110
+ "display_name": "Python 3",
111
+ "language": "python",
112
+ "name": "python3"
113
+ },
114
+ "language_info": {
115
+ "codemirror_mode": {
116
+ "name": "ipython",
117
+ "version": 3
118
+ },
119
+ "file_extension": ".py",
120
+ "mimetype": "text/x-python",
121
+ "name": "python",
122
+ "nbconvert_exporter": "python",
123
+ "pygments_lexer": "ipython3",
124
+ "version": "3.12.9"
125
+ }
126
+ },
127
+ "nbformat": 4,
128
+ "nbformat_minor": 5
129
+ }
@@ -0,0 +1,118 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 49,
6
+ "id": "116f61a2",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import numpy as np\n",
11
+ "from tqdm import tqdm\n",
12
+ "from itertools import product, combinations\n",
13
+ "from scipy.stats import binom"
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": 50,
19
+ "id": "53c66973",
20
+ "metadata": {},
21
+ "outputs": [
22
+ {
23
+ "data": {
24
+ "text/plain": [
25
+ "np.float64(0.38742048899999965)"
26
+ ]
27
+ },
28
+ "execution_count": 50,
29
+ "metadata": {},
30
+ "output_type": "execute_result"
31
+ }
32
+ ],
33
+ "source": [
34
+ "binom.pmf(1, 10, 0.1)"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "code",
39
+ "execution_count": 56,
40
+ "metadata": {},
41
+ "outputs": [
42
+ {
43
+ "name": "stderr",
44
+ "output_type": "stream",
45
+ "text": [
46
+ "100%|██████████| 32/32 [00:00<00:00, 10845.88it/s]\n",
47
+ "100%|██████████| 32/32 [00:00<00:00, 3035.09it/s]\n",
48
+ "100%|██████████| 32/32 [00:00<00:00, 1622.42it/s]\n",
49
+ "100%|██████████| 32/32 [00:00<00:00, 2020.93it/s]\n",
50
+ "100%|██████████| 32/32 [00:00<00:00, 4749.72it/s]"
51
+ ]
52
+ },
53
+ {
54
+ "name": "stdout",
55
+ "output_type": "stream",
56
+ "text": [
57
+ "0.6582226332676197\n"
58
+ ]
59
+ },
60
+ {
61
+ "name": "stderr",
62
+ "output_type": "stream",
63
+ "text": [
64
+ "\n"
65
+ ]
66
+ }
67
+ ],
68
+ "source": [
69
+ "n = 5 # n-gram\n",
70
+ "# k = 1 # no of bits modified\n",
71
+ "l = 8 # bits of watermark\n",
72
+ "p = 0.1 # probability of modification\n",
73
+ "\n",
74
+ "result = 0\n",
75
+ "for k in range(0, n + 1):\n",
76
+ " binomial_prob = binom.pmf(k, n, p)\n",
77
+ " if k == 0:\n",
78
+ " result += binomial_prob * 1\n",
79
+ " continue\n",
80
+ " results = []\n",
81
+ "\n",
82
+ " original = np.zeros(n, dtype=bool)\n",
83
+ " for original in tqdm(product([True, False], repeat=n), total=2**n):\n",
84
+ " original = np.array(original, dtype=bool)\n",
85
+ " original_agreement = original.mean() > 0.5\n",
86
+ " for modified_indices in combinations(range(n), k):\n",
87
+ " modified = original.copy()\n",
88
+ " for i in product([True, False], repeat=k):\n",
89
+ " modified[np.array(modified_indices)] = i\n",
90
+ " modified_agreement = modified.mean() > 0.5\n",
91
+ " results.append(original_agreement == modified_agreement)\n",
92
+ " result += binomial_prob * np.mean(results) ** l\n",
93
+ "print(result)"
94
+ ]
95
+ }
96
+ ],
97
+ "metadata": {
98
+ "kernelspec": {
99
+ "display_name": "Python 3",
100
+ "language": "python",
101
+ "name": "python3"
102
+ },
103
+ "language_info": {
104
+ "codemirror_mode": {
105
+ "name": "ipython",
106
+ "version": 3
107
+ },
108
+ "file_extension": ".py",
109
+ "mimetype": "text/x-python",
110
+ "name": "python",
111
+ "nbconvert_exporter": "python",
112
+ "pygments_lexer": "ipython3",
113
+ "version": "3.12.9"
114
+ }
115
+ },
116
+ "nbformat": 4,
117
+ "nbformat_minor": 5
118
+ }
@@ -141,6 +141,7 @@ class Watermarker:
141
141
  do_sample=do_sample,
142
142
  logits_processor=logits_processor,
143
143
  pad_token_id=self.tokenizer.eos_token_id,
144
+ tokenizer=self.tokenizer,
144
145
  **kwargs
145
146
  )
146
147
  output = output[:,tokd_input["input_ids"].shape[-1]:].cpu()
@@ -1,10 +1,12 @@
1
1
  import argparse
2
2
  import logging
3
3
  import os
4
+ import gc
4
5
  import torch
5
6
  from typing import List, Literal, Optional, Tuple
6
7
 
7
8
  from transformers import AutoTokenizer, AutoModelForCausalLM
9
+ from transformers.modeling_utils import PreTrainedModel
8
10
  from sentence_transformers import SentenceTransformer
9
11
  from tqdm.auto import tqdm
10
12
 
@@ -21,6 +23,8 @@ PROMPT = (
21
23
  )
22
24
  PRE_PARAPHRASED = "Here is a paraphrased version of the text while preserving the semantic similarity:\n\n"
23
25
 
26
+ waterfall_cached_watermarking_model = None # Global variable to cache the watermarking model
27
+
24
28
  def detect_gpu() -> str:
25
29
  """
26
30
  Use torch to detect if MPS, CUDA, or neither (default CPU)
@@ -42,9 +46,10 @@ def watermark(
42
46
  sts_model: SentenceTransformer,
43
47
  num_beam_groups: int = 4,
44
48
  beams_per_group: int = 2,
45
- STS_scale:float = 2.0,
49
+ STS_scale: float = 2.0,
46
50
  diversity_penalty: float = 0.5,
47
51
  max_new_tokens: Optional[int] = None,
52
+ **kwargs
48
53
  ) -> str:
49
54
  paraphrasing_prompt = watermarker.tokenizer.apply_chat_template(
50
55
  [
@@ -61,6 +66,7 @@ def watermark(
61
66
  num_beam_groups = num_beam_groups,
62
67
  num_return_sequences = num_beam_groups * beams_per_group,
63
68
  diversity_penalty = diversity_penalty,
69
+ **kwargs,
64
70
  )
65
71
 
66
72
  # Select best paraphrasing based on q_score and semantic similarity
@@ -75,7 +81,8 @@ def watermark(
75
81
  def verify_texts(texts: List[str], id: int,
76
82
  watermarker: Optional[Watermarker] = None,
77
83
  k_p: Optional[int] = None,
78
- model_path: Optional[str] = "meta-llama/Llama-3.1-8B-Instruct"
84
+ model_path: Optional[str] = "meta-llama/Llama-3.1-8B-Instruct",
85
+ return_extracted_k_p: bool = False
79
86
  ) -> Tuple[float,float]:
80
87
  """Returns the q_score and extracted k_p"""
81
88
 
@@ -87,7 +94,11 @@ def verify_texts(texts: List[str], id: int,
87
94
  if k_p is None:
88
95
  k_p = watermarker.k_p
89
96
 
90
- verify_results = watermarker.verify(texts, id=[id], k_p=[k_p], return_extracted_k_p=True) # results are [text x id x k_p]
97
+ verify_results = watermarker.verify(texts, id=[id], k_p=[k_p], return_extracted_k_p=return_extracted_k_p) # results are [text x id x k_p]
98
+
99
+ if not return_extracted_k_p:
100
+ return verify_results[:,0,0]
101
+
91
102
  q_score = verify_results["q_score"]
92
103
  k_p_extracted = verify_results["k_p_extracted"]
93
104
 
@@ -140,6 +151,7 @@ def watermark_texts(
140
151
  diversity_penalty: float = 0.5,
141
152
  STS_scale:float = 2.0,
142
153
  use_tqdm: bool = False,
154
+ stop_at_double_newline: bool = True, # if True, will stop generation at the first double newline. Prevent repeated paraphrasing of the same text.
143
155
  ) -> List[str]:
144
156
  if watermark_fn == 'fourier':
145
157
  watermarkingFnClass = WatermarkingFnFourier
@@ -150,11 +162,24 @@ def watermark_texts(
150
162
 
151
163
  if watermarker is None:
152
164
  assert model_path is not None, "model_path must be provided if watermarker is not passed"
153
- model = AutoModelForCausalLM.from_pretrained(
154
- model_path,
155
- torch_dtype=torch_dtype,
156
- device_map=device,
157
- )
165
+ global waterfall_cached_watermarking_model
166
+
167
+ if isinstance(waterfall_cached_watermarking_model, PreTrainedModel) and waterfall_cached_watermarking_model.name_or_path != model_path:
168
+ device = waterfall_cached_watermarking_model.device.type
169
+ waterfall_cached_watermarking_model = None
170
+ gc.collect()
171
+ if device == "cuda":
172
+ torch.cuda.empty_cache()
173
+ elif device == "mps":
174
+ torch.mps.empty_cache()
175
+
176
+ if waterfall_cached_watermarking_model is None:
177
+ waterfall_cached_watermarking_model = AutoModelForCausalLM.from_pretrained(
178
+ model_path,
179
+ torch_dtype=torch_dtype,
180
+ device_map=device,
181
+ )
182
+ model = waterfall_cached_watermarking_model
158
183
  tokenizer = AutoTokenizer.from_pretrained(model_path)
159
184
 
160
185
  watermarker = Watermarker(tokenizer=tokenizer, model=model, id=id, kappa=kappa, k_p=k_p, watermarkingFnClass=watermarkingFnClass)
@@ -173,6 +198,9 @@ def watermark_texts(
173
198
  T_ws = []
174
199
 
175
200
  for T_o in tqdm(T_os, desc="Watermarking texts", disable=not use_tqdm):
201
+ if stop_at_double_newline and "\n\n" in T_o:
202
+ logging.warning("Text contains \\n\\n and stop_at_double_newline is set to True, replacing all \\n\\n in text.")
203
+ T_o = T_o.replace("\n\n", " ") # replace double newlines with space
176
204
  T_w = watermark(
177
205
  T_o,
178
206
  watermarker = watermarker,
@@ -181,6 +209,7 @@ def watermark_texts(
181
209
  beams_per_group = beams_per_group,
182
210
  diversity_penalty = diversity_penalty,
183
211
  STS_scale = STS_scale,
212
+ stop_strings=["\n\n"] if stop_at_double_newline else None,
184
213
  )
185
214
  T_ws.append(T_w)
186
215
 
@@ -283,7 +312,7 @@ def main():
283
312
  )
284
313
 
285
314
  # watermarker = Watermarker(tokenizer=tokenizer, model=None, id=id, k_p=k_p, watermarkingFnClass=watermarkingFnClass) # If only verifying the watermark, do not need to instantiate the model
286
- q_scores, extracted_k_ps = verify_texts(T_os + T_ws, id, watermarker, k_p=k_p)
315
+ q_scores, extracted_k_ps = verify_texts(T_os + T_ws, id, watermarker, k_p=k_p, return_extracted_k_p=True)
287
316
 
288
317
  for i in range(len(T_os)):
289
318
  # Handle the case where this is being run
File without changes
File without changes
File without changes
File without changes